Thinking
Show when an assistant is working, with a typing indicator and collapsible reasoning.
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Two parts cover the wait. ChatTypingIndicator says the assistant is working when nothing has arrived yet. ChatReasoning shows a model's reasoning as it streams, then folds it under "Thought for 4s" so the answer takes the space. Put either inside the assistant's Chat Message, before its content. For work with a name and a result, like a search or an API call, use a Tool Call instead.
import { ChatMessage, ChatReasoning, ChatTypingIndicator } from '@youj/react'
<ChatMessage role="assistant" content={reply.stream} streaming={reply.streaming}>
{reply.waiting && <ChatTypingIndicator />}
{reply.reasoning && (
<ChatReasoning content={reply.reasoning.stream} streaming={reply.reasoning.streaming} duration={reply.reasoning.seconds} />
)}
</ChatMessage>Description
Reasoning that streams open, folds to “Thought for” its duration, and can be run again.
ChatReasoning streams its content as muted markdown under a rule. Uncontrolled, it's open while streaming and folds away when the thinking is done, until the reader toggles it: then their choice sticks. The label reads "Thinking…" with a sweep of light while it streams, then "Thought for" and duration (in seconds), or "Reasoning" without one. Control it with open and onOpenChange.
The two typing indicators, shimmer and dots.
The default shimmer sweeps a band of light across its label. dots is three dots in a traveling wave, for tighter spaces, with the label read to screen readers only. label defaults to "Thinking…". The sweep pauses while it's offscreen or the tab is hidden.
Code
thinking-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propscontent, streaming, duration… (7)
Props
| Prop | Type | |
|---|---|---|
contentRequired | ChatTextThe reasoning, as markdown: a string, or a ChatTextSource streaming it. | – |
streaming | booleanStill thinking: the label sweeps and the text streams in. default false | false |
duration | numberHow long the thinking took, in seconds: the folded label says so. | – |
open | booleanControlled. Uncontrolled, it is open while streaming and folds away when done, until the reader toggles it. | – |
onOpenChange | (open: boolean) => void | – |
Other props pass through: extends ComponentProps<'div'>. | ||
ChatTypingIndicator
| Prop | Type | |
|---|---|---|
variant | 'shimmer' | 'dots''shimmer' sweeps light across the label; 'dots' is three dots in a wave. default 'shimmer' | 'shimmer' |
label | stringShown and announced. default "Thinking…" | "Thinking…" |
Other props pass through: extends ComponentProps<'div'>. | ||
MotionShimmer sweep, dots, reasoning folds… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Shimmer sweepa masked copy of the label rides a band across it; transform only | duration.deliberateease.standard560ms · cubic-bezier(0.2, 0, 0, 1) · × 3 per sweep | 560mscubic-bezier(0.2, 0, 0, 1)× 3 per sweep | |
| Dotsthree dots in a traveling wave | no token loop | loop | |
Reasoning foldsheight to and from auto, the text fading with it (Collapse) | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Fold fade outfaster than the fold, so text never looks squashed | duration.fastease.exit140ms · cubic-bezier(0.4, 0, 1, 1) | 140mscubic-bezier(0.4, 0, 1, 1) | |
| Label swap"Thinking…" crossfades to "Thought for 4s" (Morph Label) | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Label width | spring.snappy0.2s, bounce 0 | 0.2s, bounce 0 |
Reduced motion
The label holds still (no sweep, no wave), and the reasoning opens and closes instantly.
Reduced motion
The shimmer and the dots hold still, the label swaps without crossfading, and the reasoning opens and folds at once. The reasoning text arrives in whole sentences, as all streamed markdown does.
AccessibilityARIA, announcements
Accessibility
- The reasoning's toggle is a button with
aria-expandedandaria-controls, named by its label. - Inside a Chat Message List, a typing indicator is announced once through the list, unless it sits in a message, which already said it's responding. On its own it's a
statusregion. - The shimmer's moving copy is
aria-hidden, so the label is read once.