Reasoning
betaCollapsible thinking trace: a shimmering “Thinking… 12s” label while active that settles into “Thought for 14s”, revealing muted reasoning text.
Finished trace
minimal
Thought for 14s
The user wants the like button to feel instant. The lag comes from waiting on the server round-trip before re-rendering.
Options: optimistic local state with manual rollback, or useOptimistic inside a transition. useOptimistic handles the rollback automatically if the action throws, and keeps the UI consistent with concurrent updates.
I should recommend useOptimistic and show the minimal pattern, then mention the error case.
Live thinking that auto-collapses
Streams the trace with StreamingText while active (controlled open), then collapses to “Thought for Ns”.
full
Thinking
Above an assistant answer
Inside a Message, with a custom label.
composition
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/reasoning.jsonCopies the source into components/kura/reasoning with its dependencies (kura-theme, shimmer-text, use-controllable-state, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- Show a reasoning model's chain of thought / thinking blocks (Claude extended thinking, o-series reasoning summaries) above the answer.
- Indicate that the model is thinking, with elapsed time, before the first answer token arrives.
- Keep a finished trace available but out of the way (collapsed to one line).
Reach for something else when
- A bare “Thinking…” label with no trace to reveal — use shimmer-text (or thinking-indicator).
- A sequence of discrete agent steps with states — use agent-timeline.
- A single tool invocation with arguments and results — use tool-call.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | false | The model is still thinking: the label shimmers and an elapsed timer ticks. Turning it false settles the label and (with autoCollapse) collapses the trace. |
| duration | number | — | Seconds spent thinking, if you know it (e.g. stored with the message). Otherwise measured while `active`. |
| open | boolean | — | Expanded (controlled). |
| defaultOpen | boolean | — | Initial expanded state when uncontrolled. Defaults to `active`: open while thinking, closed for finished traces. |
| onOpenChange | (open: boolean) => void | — | Called when the trace is expanded/collapsed (by the reader or by auto-collapse). |
| autoCollapse | boolean | true | Collapse when `active` turns false — skipped if the reader toggled it while it was active. |
| formatLabel | (state: { active: boolean; seconds: number }) => React.ReactNode | — | Custom label (i18n, “Planning…”). Default “Thinking… 12s” / “Thought for 14s” / “Thought for a moment”. |
All native attributes of the root element, className and ref pass through.
Anatomy
- ReasoningRoot. `data-status="active" | "done"`, `data-state="open" | "closed"`.
data-slot="reasoning" - TriggerButton with the label and a chevron that rotates 90° when open (aria-expanded, aria-controls).
data-slot="reasoning-trigger" - LabelShimmerText; shimmers while active, muted when done. Seconds are tabular.
data-slot="reasoning-label" - ContentMuted, pre-wrapped trace behind a left hairline. Animates grid-template-rows 0fr↔1fr (interruptible) and is inert when closed.
data-slot="reasoning-content"
Accessibility
- The trigger is a real button with aria-expanded/aria-controls.
- A visually hidden status region says “Thinking” and then “Thought for 14s” — the ticking seconds are never announced.
- When collapsed, the trace is `inert`: out of the tab order and the accessibility tree.
- Reduced motion: The shimmer becomes static muted text, the chevron snaps, and the trace only fades (no height travel or blur).
| Keys | Action |
|---|---|
| Enter / Space | Expand or collapse the trace |
Notes for agents
- Keep the component mounted from the first thinking token to the end; flip `active` instead of swapping components, so the timer and collapse work.
- Persist the measured duration with the message and pass it as `duration` when re-rendering history.
- Put a StreamingText (caret={false}) inside for a live trace; plain text or markdown for a stored one.