OperantAI Workflow Design System
ComfortableCompact

Response is the container for AI-generated output — the primary way Operant renders model output as opposed to plain prose (per the AGENTS.md decision table: "Live/complete AI output → .ods-response (-streaming)", not a bare <p>). It has four states — thinking, streaming, done, error — and announces state changes to assistive technology automatically.

Installation

HTML — already included if you loaded the 3 stylesheets — see the Installation guide.

React

npm install @operant/ui

@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.

Usage

HTML

<div class="ods-response" role="status" aria-live="polite">
  <div class="ods-response-prose">
    Error rate on checkout-svc rose from 0.3% to 7.1% starting 14:02 UTC.
  </div>
</div>

React

import { Response } from "@operant/ui";

export default function Example() {
  return (
    <Response
      status="done"
      content="Error rate on checkout-svc rose from 0.3% to 7.1% starting 14:02 UTC."
    />
  );
}

Examples

Thinking

While the model is working and has no output yet, show the animated three-dot .ods-thinking-dots indicator next to a visible label. The label is required — per AGENTS.md #4/#8, color or motion is never the only signal for a state.

HTML

<div class="ods-response" role="status" aria-live="polite" aria-busy="true">
  <span class="ods-thinking">
    <span class="ods-thinking-dots"><i></i><i></i><i></i></span>
    Thinking…
  </span>
</div>

React

<Response status="thinking" />
{/* Custom label: <Response status="thinking" thinkingLabel="Analyzing logs…" /> */}

Streaming

While tokens are actively arriving, the container gains the .-streaming modifier (border tints to --color-primary-border) and a blinking .ods-caret is appended after the text.

HTML

<div class="ods-response -streaming" role="status" aria-live="polite" aria-busy="true">
  <div class="ods-response-prose">
    Error rate on checkout-svc rose from 0.3% to 7.1% starting 14:02 UTC<span class="ods-caret" aria-hidden="true"></span>
  </div>
</div>

React

<Response
  status="streaming"
  content="Error rate on checkout-svc rose from 0.3% to 7.1% starting 14:02 UTC"
/>

Done

Once complete, the border returns to the default --color-border and the caret disappears. role="status"/aria-live="polite" remain in place — aria-live only announces on change, so a settled response does not re-announce itself on every render; it fires once for the final thinking → streaming → done transition.

HTML

<div class="ods-response" role="status" aria-live="polite">
  <div class="ods-response-prose">
    Error rate on checkout-svc rose from 0.3% to 7.1% starting 14:02 UTC, coinciding exactly with
    the deploy of v2.31.0. Rolling back to the last healthy release v2.30.4 is low-blast-radius
    and reversible.
  </div>
</div>

React

<Response
  status="done"
  content="Error rate on checkout-svc rose from 0.3% to 7.1% starting 14:02 UTC, coinciding exactly with the deploy of v2.31.0. Rolling back to the last healthy release v2.30.4 is low-blast-radius and reversible."
/>

Error

Errors escalate to role="alert" (and drop aria-live, since alert is implicitly assertive) so screen readers interrupt to announce the failure immediately.

HTML

<div class="ods-response" role="alert">
  <div class="ods-response-prose">
    The agent run failed: tool call to deploy timed out after 30s.
  </div>
</div>

React

<Response
  status="error"
  content="The agent run failed: tool call to deploy timed out after 30s."
/>

API Reference

<Response>

PropTypeDefaultDescription
status"thinking" | "streaming" | "done" | "error"—Required. Drives the visual state, ARIA role, and aria-busy.
contentstring—Plain-string body. Ignored (in favor of children) while status === "thinking".
childrenReactNode—Rich alternative to content — e.g. pre-rendered markdown nodes.
thinkingLabelstring"Thinking…"Visible label shown next to the thinking dots while status === "thinking".
classNamestring—Merged with .ods-response.

Internally, status === "error" sets role="alert" (no aria-live); every other status sets role="status" with aria-live="polite". aria-busy is set while status is "thinking" or "streaming".

HTML-side classes: .ods-response (the card — surface background, hairline border, radius-lg), .-streaming modifier (tints the border to --color-primary-border), .ods-response-prose (the text body — capped at --w-reading-max, --fs-lg, relaxed line height), .ods-caret (blinking streaming cursor), .ods-thinking / .ods-thinking-dots (the thinking indicator, colored --color-agent-active).

Accessibility

  • status="thinking" | "streaming" | "done" all render role="status" + aria-live="polite" — assistive tech is notified of the transition without interrupting the user, per AGENTS.md #8 ("streaming/thinking/complete announced via role=\"status\"").
  • status="error" renders role="alert" instead, which is implicitly assertive — screen readers interrupt current speech to announce it, per AGENTS.md #8 ("errors via role=\"alert\"").
  • aria-busy is set to true during thinking and streaming, giving assistive tech an explicit signal beyond the visual caret/dots.
  • The blinking caret (.ods-caret) and thinking dots (.ods-thinking-dots) are decorative motion cues layered on top of the text/label state — they are never the only signal (AGENTS.md #4), and both respect prefers-reduced-motion via the token system.
  • Because aria-live only announces on value change, a Response that mounts already in status="done" with static content will not spuriously read itself aloud — only actual thinking → streaming → done transitions get announced.