Reasoning
A disclosure for the model's thinking. Reasoning opens itself while a stream is arriving
and closes itself once it stops, and reports how long the thinking took.
Import
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
} from '@elirobinson/ai-elements/components/reasoning';
import {
ChainOfThought,
ChainOfThoughtContent,
ChainOfThoughtHeader,
ChainOfThoughtStep,
} from '@elirobinson/ai-elements/components/chain-of-thought';
import { Shimmer } from '@elirobinson/ai-elements/components/shimmer';How it opens and closes
isStreaming drives the whole thing, and the rules are worth reading before you fight them:
- It auto-opens whenever
isStreamingis true and it is closed — unless you passeddefaultOpen={false}, which is the one opt-out. This effect re-runs, so a reader who closes the panel mid-stream has it reopened. - It auto-closes one second after
isStreaminggoes false, and only once per mount. After that single auto-close, reopening the panel sticks. durationis measured from when streaming started to when it stopped, in whole seconds, and the trigger reads "Thought for N seconds" — or "Thought for a few seconds" when there is no duration to report, which is what the demo above shows. Passdurationto control it.- While streaming, the trigger's label is a
Shimmerreading "Thinking...".
ReasoningContent takes a string and renders it as markdown through Streamdown.
Chain of thought
ChainOfThoughtStep takes label, an optional description, an icon, and a status of
complete, active or pending, which is styled rather than announced — the status is
colour and icon only.
Motion
Shimmer is the sweeping-highlight text used for "Thinking..." and for a streaming
PlanTitle. It animates through Framer Motion on an infinite linear repeat.
Thinking
Nothing in the vendored tree reads prefers-reduced-motion — not Shimmer, and not the
animate-in entrance each ChainOfThoughtStep plays. The demos on this page are frozen so
the visual suite can photograph them, and freezing Shimmer in particular takes more than
the two obvious levers: the stage's animation: none is CSS-only and Framer Motion does not
use CSS animation, and MotionConfig's reducedMotion="always" drops only transform and
layout animations — Shimmer animates background-position, so it survives both. The stage
pins that one property with !important, which beats the value Framer writes inline.
If you are honouring reduced motion, reducedMotion="always" alone will not stop this
component. In your app it moves until you stop it yourself, at the boundary.
Accessibility
ReasoningTriggerandChainOfThoughtHeaderare full-width disclosure rows, both floored tovar(--target)— see below. Both come from the shared collapsible, soEnterandSpacetoggle them andaria-expandedfollows the state.- Neither component sets
aria-live— and neither does anything else in the package: there is no explicitaria-liveattribute in the vendored tree. Three roles imply one, and none of them is on this page:Conversation'srole="log", the shared alert'srole="alert"(whichConfirmationrenders — see Tools and tasks), and the shared spinner'srole="status". So whether a reasoning stream is announced is decided by where you render it and by thearia-liveyou put on the thread — see Conversation and messages.
What this system changed
| Patch | What changed, and why |
|---|---|
reasoning-trigger-floor | primary — reasoning.tsx — ReasoningTrigger. Measured 1248x20. Same shape as SourcesTrigger: a full-width row at 20px. |
chain-of-thought-header-floor | primary — chain-of-thought.tsx — ChainOfThoughtHeader. Measured 1248x20. Same shape again. |
Read from vendoredElementTargets in @elirobinson/ai-patterns/contracts, which the browser audit writes.