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:

  • isStreaming flips true → auto-open. The panel also starts open when it mounts mid-stream — the uncontrolled default is isStreaming — and an explicit defaultOpen={false} wins only for the first frame before the auto-open effect overrides it.
  • isStreaming flips 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-expanded and aria-controls pointing at the panel, and the caret rotation is pure CSS off aria-expanded.
  • The panel stays mounted for the height animation but is inert while 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 transition override.

Props

ComponentPropTypeDefaultDescription
ReasoningisStreamingbooleanfalseWhether reasoning tokens are still streaming — drives auto-open/auto-collapse, the thinking clock, and the trigger label.
Reasoningdurationnumberauto-trackedThinking time in seconds, controlled. When omitted, tracked from the isStreaming flips.
ReasoningautoOpenbooleantrueWhether 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.
Reasoningopen / defaultOpen / onOpenChangeCollapsible propsdefaultOpen ?? (autoOpen && isStreaming)Usual Radix semantics. In controlled mode the auto behaviour arrives as onOpenChange requests.
ReasoningTriggerstreamingLabelstringLive 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.
ReasoningTriggerchildrenReactNodeicon + label + caretReplace the default trigger content entirely.
ReasoningContentchildrenReactNodeThe trace — plain text or a composed Response.
ReasoningContenttransitionTransitionhouse springOverride 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.
ReasoningContentclassNamestringStyles the inner padded box (the quiet trace register), not the animated wrapper.

On this page