While a model reasons, show one line that says what it is doing ("Reading the incident timeline") with a live timer. Put the steps or a reasoning summary behind a closed fold. When the answer starts, stop the timer and collapse the line to "Thought for 12s", still openable, so the wait is explained and the answer stays on top.
Why a spinner is not enough#
Reasoning models can think for several seconds to minutes before the first word of the answer. A spinner over that gap says nothing, so people wonder whether it froze and press Stop or retry. A line that names the current phase and counts the seconds tells them the work is real and roughly how long it takes.
What to show#
- A phase label that does not lie. Use what you know: the tool being called, the document being read, or the provider's reasoning summary heading. Do not cycle fake labels ("Pondering… Synthesizing…") on a timer; people notice.
- Elapsed time. Seconds, ticking. It sets expectations better than any progress bar you cannot compute.
- Steps on request. A fold that lists what was done, with the current step in full colour and finished steps checked. Closed by default; most people only want the answer.
- A finished state. "Thought for 12s", clickable to reopen the steps. Do not delete the line when the answer arrives; it is the explanation for the wait.
Raw reasoning or a summary#
Some providers return reasoning summaries, some return the reasoning text, some return nothing but timing. Show what you have, and prefer summaries in product UI: raw chains of thought are long, repetitive and sometimes contradict the final answer, which confuses more than it explains. Keep raw reasoning for debugging views.
Motion and accessibility#
A slow shimmer across the label is enough to show life; stop it when done and under prefers-reduced-motion. Give the fold a real button with aria-expanded, and do not announce every label change to screen readers. Announce once when the answer starts.
With PixilUI#
ThinkingState is the line, the timer, the fold and the finished state. Feed it the label and steps you have, and set done when the answer starts streaming.
import { ThinkingState, type ThinkingStateStep } from "@/components/pixilui/thinking-state";
export function Reasoning({ label, steps, startedAt, answerStarted }: { label: string; steps: ThinkingStateStep[]; startedAt: number; answerStarted: boolean }) {
return (
<ThinkingState
label={label}
steps={steps}
startedAt={startedAt}
done={answerStarted}
doneLabel={(ms) => (ms === null ? "Thought it through" : `Thought for ${Math.round(ms / 1000)}s`)}
/>
);
}
// steps: [{ id: "read", text: "Reading the incident timeline", status: "done" },
// { id: "compare", text: "Comparing p95 latency before and after deploy 4f3a9c1", status: "active" }]Install it with npx shadcn@latest add @pixilui/thinking-state. Pair it with streaming text for the answer that follows.
When not to use this#
- Fast models. If the first token arrives within a second, a thinking line flashes and disappears. Show nothing, or a caret.
- Tool-heavy agents. When the wait is mostly tool calls, show tool call rows; they carry real evidence, not a label.
- Long background runs. Minutes of autonomous work belong in a run timeline the person can leave and come back to.
- When you have no honest label. A plain "Thinking" with a timer is better than invented phases.