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>
| Prop | Type | Default | Description |
|---|---|---|---|
status | "thinking" | "streaming" | "done" | "error" | — | Required. Drives the visual state, ARIA role, and aria-busy. |
content | string | — | Plain-string body. Ignored (in favor of children) while status === "thinking". |
children | ReactNode | — | Rich alternative to content — e.g. pre-rendered markdown nodes. |
thinkingLabel | string | "Thinking…" | Visible label shown next to the thinking dots while status === "thinking". |
className | string | — | 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 renderrole="status"+aria-live="polite"— assistive tech is notified of the transition without interrupting the user, perAGENTS.md#8 ("streaming/thinking/complete announced viarole=\"status\"").status="error"rendersrole="alert"instead, which is implicitly assertive — screen readers interrupt current speech to announce it, perAGENTS.md#8 ("errors viarole=\"alert\"").aria-busyis set totrueduringthinkingandstreaming, 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 respectprefers-reduced-motionvia the token system. - Because
aria-liveonly announces on value change, aResponsethat mounts already instatus="done"with static content will not spuriously read itself aloud — only actual thinking → streaming → done transitions get announced.