Skip to content

Thinking

Show when an assistant is working, with a typing indicator and collapsible reasoning.

Installation & usage

npm i @youj/react

For 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>

Reference

Propscontent, streaming, duration… (7)

Props

ChatReasoning props
PropType
contentRequiredChatText

The reasoning, as markdown: a string, or a ChatTextSource streaming it.

–
streamingboolean

Still thinking: the label sweeps and the text streams in.

default false

false
durationnumber

How long the thinking took, in seconds: the folded label says so.

–
openboolean

Controlled. 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

ChatTypingIndicator props
PropType
variant'shimmer' | 'dots'

'shimmer' sweeps light across the label; 'dots' is three dots in a wave.

default 'shimmer'

'shimmer'
labelstring

Shown and announced.

default "Thinking…"

"Thinking…"
Other props pass through: extends ComponentProps<'div'>.
MotionShimmer sweep, dots, reasoning folds… · reduced motion

Motion

Motion tokens
MomentTokens
Shimmer sweepa masked copy of the label rides a band across it; transform only
duration.deliberateease.standard

560ms · 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.standard

220ms · 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.exit

140ms · 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.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
Label width
spring.snappy

0.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-expanded and aria-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 status region.
  • The shimmer's moving copy is aria-hidden, so the label is read once.