Reasoning
The collapsible thinking trace — auto-opens while reasoning tokens stream, auto-collapses once they settle, and hands control to the user on first toggle.
<Reasoning isStreaming={isStreaming}> <ReasoningTrigger /> <ReasoningContent> <Response>{reasoningText}</Response> </ReasoningContent></Reasoning>Usage
import { Reasoning, ReasoningContent, ReasoningTrigger } from "@reva/ui";Reasoning is the disclosure for a reply's thinking trace, built on the house Collapsible with a Motion-driven height animation. One open state lives on the root, with the usual open / defaultOpen / onOpenChange semantics and two automatic inputs layered on top:
isStreamingflips true → auto-open. The panel also starts open when it mounts mid-stream — the uncontrolled default isisStreaming— and an explicitdefaultOpen={false}wins only for the first frame before the auto-open effect overrides it.isStreamingflips false → auto-collapse, after a short grace (~800ms) so the finished trace registers before it folds away.
Manual toggles take over permanently. Trigger interaction is the manual signal: once the user toggles, all auto behaviour stops for that instance — a toggle landing inside the grace window beats the pending collapse. Auto-collapse also only arms on the streaming→idle transition, so re-opening a settled trace never re-closes it.
Controlled mode gets requests, not flips. When open is passed, the auto behaviour never changes state directly — it surfaces as onOpenChange requests (true on stream start, false after the grace) which the parent may apply or ignore. Wiring the standard onOpenChange={setOpen} therefore preserves the full auto behaviour (see the controlled example below).
The trigger reads a Shimmer-swept "Thinking…" while streaming and "Thought for …" after. Pass duration (seconds) as a controlled value, or omit it and the thinking time is auto-tracked from the isStreaming flips. Humanization keeps the label calm: unknown or under 5s reads "a few seconds", 5s to under 120s reads "N seconds", and from 120s it reads "N minutes" (rounded — the smallest it can render is "2 minutes", so no singular form is ever needed).
For styling, the root and trigger carry data-streaming while tokens stream (the content does not — style it via a descendant selector off the root), and all three parts carry Radix's data-state="open" | "closed".
Composition
Use the following composition to build a Reasoning:
Reasoning (isStreaming, duration — owns the open-state model)
├── ReasoningTrigger (Brain · "Thinking…" / "Thought for …" · rotating caret)
└── ReasoningContent (always mounted, inert while closed)
└── Response (the trace markdown — or any composed content)Examples
Supplied duration
When the transport reports thinking time itself, pass duration — it takes precedence over the auto-tracked clock. defaultOpen mounts the settled trace expanded.
Started from the mandate: tolerance is ±5% per asset class, reviewed quarterly. The Patel SIPP's bond weight sits 4.2% under target — inside the letter of the band, but trending out for two consecutive quarters, so the right call is to surface it now with a buy-side rebalance sketched rather than wait for a breach.
<Reasoning duration={42} defaultOpen> <ReasoningTrigger /> <ReasoningContent> <Response>{trace}</Response> </ReasoningContent></Reasoning>Controlled open state
The standard controlled wiring keeps the auto lifecycle — the auto inputs arrive as onOpenChange requests, and applying them verbatim reproduces the uncontrolled behaviour. Gate or ignore them to take over (e.g. a "keep reasoning open" workspace preference).
const [open, setOpen] = useState(false);
// `onOpenChange` fires with `true` when the stream starts and `false` after
// the post-stream grace, as well as on trigger clicks — apply or ignore.
<Reasoning open={open} onOpenChange={setOpen} isStreaming={isStreaming}>
<ReasoningTrigger />
<ReasoningContent>{trace}</ReasoningContent>
</Reasoning>;Accessibility
- Radix Collapsible wiring is intact: the trigger carries
aria-expandedandaria-controlspointing at the panel, and the caret rotation is pure CSS offaria-expanded. - The panel stays mounted for the height animation but is
inertwhile closed — removed from the accessibility tree and tab order, the forceMount equivalent of Radix's own hidden state. - SSR-stable: the resolved open/closed styles render inline on the server, so hydration is clean and nothing animates on mount.
- Reduced motion branches values only: the expand/collapse spring collapses to zero duration, the caret skips its transition, and the streaming label's Shimmer falls back to plain muted text. The markup never forks. For this height-animating panel the zero-duration collapse is enforced — it wins even over a consumer
transitionoverride.
Props
| Component | Prop | Type | Default | Description |
|---|---|---|---|---|
Reasoning | isStreaming | boolean | false | Whether reasoning tokens are still streaming — drives auto-open/auto-collapse, the thinking clock, and the trigger label. |
Reasoning | duration | number | auto-tracked | Thinking time in seconds, controlled. When omitted, tracked from the isStreaming flips. |
Reasoning | autoOpen | boolean | true | Whether streaming drives the open state. Pass false for a disclosure that stays closed unless the user opens it — the streaming affordance then lives on the trigger label. |
Reasoning | open / defaultOpen / onOpenChange | Collapsible props | defaultOpen ?? (autoOpen && isStreaming) | Usual Radix semantics. In controlled mode the auto behaviour arrives as onOpenChange requests. |
ReasoningTrigger | streamingLabel | string | — | Live status line shown while streaming: renders inside the Shimmer sweep in place of "Thinking…" and drops the Brain icon. Ignored once streaming ends (the settled "Thought for …" title wins) and when children are passed. |
ReasoningTrigger | children | ReactNode | icon + label + caret | Replace the default trigger content entirely. |
ReasoningContent | children | ReactNode | — | The trace — plain text or a composed Response. |
ReasoningContent | transition | Transition | house spring | Override the expand/collapse spring. Under reduced motion the zero-duration transition is enforced — it beats this override. initial / animate / exit are owned by the component. |
ReasoningContent | className | string | — | Styles the inner padded box (the quiet trace register), not the animated wrapper. |