Skip to content

Thinking

A collapsible reasoning trace with elapsed time.

Status
Beta
Category
AI
Adoption
Not used yet
import { Thinking } from "@oration/canon/components/ai/thinking";
packages/canon/src/components/ai/thinking.tsx
Thinking
import { Thinking } from "@oration/canon/components/ai/thinking";import * as React from "react";export function Hero() {    const steps = [        { label: "Reading the ticket and 3 earlier messages" },        {            label: "Checking the payment in the ERP",            detail: "PMT-58213 went out on September 25 by ACH.",        },        {            label: "Matching Northwind Freight's early-pay terms",            detail: "2/10 net 30, enrolled in March.",        },        { label: "Drafting a reply" },    ];    const [startedAt] = React.useState(() => Date.now());    const [shown, setShown] = React.useState(1);    const [thoughtFor, setThoughtFor] = React.useState<number | undefined>();    React.useEffect(() => {        if (thoughtFor !== undefined) return;        const id = window.setTimeout(() => {            if (shown < steps.length) setShown((n) => n + 1);            else                setThoughtFor(                    Math.max(1, Math.round((Date.now() - startedAt) / 1000)),                );        }, 900);        return () => window.clearTimeout(id);    }, [shown, steps.length, thoughtFor, startedAt]);    return (        <div className="flex w-full max-w-lg flex-col gap-3 rounded-xl bg-card p-4 text-left shadow-border">            <Thinking                steps={steps.slice(0, shown)}                status={thoughtFor === undefined ? "running" : "done"}                startedAt={startedAt}                elapsed={thoughtFor}            />            {thoughtFor !== undefined ? (                <p className="text-sm leading-relaxed text-pretty text-foreground">                    The $412.50 difference on INV-20931 is Northwind Freight's                    2% early-payment discount. Payment PMT-58213 went out on                    September 25 for $20,212.50, and the remittance advice lists                    the discount as its own line.                </p>            ) : null}        </div>    );}

Usage#

Thinking is a one-line, collapsible reasoning trace. While the model works it reads Thinking… 4s with a small pixel grid and the latest step beside it; when it finishes it settles to Thought for 6s. Opening it shows every step on a thin rail, each with an optional one-line detail. It exists so people can check how an answer was reached without the reasoning competing with the answer. The common mistake is opening it by default: the trace stays folded above the answer, and the answer leads.

When to use

  • At the top of a Copilot answer, before tool calls and the streamed text.
  • Above a retrieval answer, such as a knowledge base test, to show how the passages were found.
  • When the steps are worth auditing later: which calls were read, which rules were checked.
  • When the wait is long enough that the latest step reassures people something is happening.

When not to use

  • For a wait with nothing to show but a label and a timer. Use AI loader
  • For a plan the person will watch execute, where steps can fail or be skipped. Use Task list
  • For tool calls with arguments and results. Use Tool chip
  • For the answer itself. Use Streaming text
  • For a proposed change that needs a decision. Use Approval card

The answer leads

Reasoning is folded by default and sits above the answer as one quiet 13px line. It opens only when someone asks, so AI help arrives without noise.

Shimmer only while working

The shimmer runs on Thinking… and on the current step, and stops as soon as the trace is done. Finished steps are plain text.

Anatomy#

Thinking
  1. Read the ticket and 3 earlier messages
  2. Checked the payment in the ERPPMT-58213 went out on September 25.
Thought for 6 seconds
  1. Pixel grid. The small 3×3 grid from AI loader at 80% opacity. It ripples while running and rests when done.
  2. Summary. Thinking… (shimmering) or Thought for, in medium weight, then the elapsed time in regular tabular figures.
  3. Chevron. Present when there are steps. Turns 90° when open.
  4. Latest step. While running and folded, the newest step trails the summary in Slate Meta and truncates.
  5. Rail. A 1px hairline on the left of the open list, aligned under the grid.
  6. Step. 13px text. The current step shimmers in medium weight while running; the rest are ink at 80%.
  7. Detail. Optional 12px Slate Meta line under a step.

Examples#

Running and done

While running, the summary ticks and the latest step trails it. Done, it settles to one quiet line with the total time.

Thinking
Thought for 6 seconds
import { Thinking } from "@oration/canon/components/ai/thinking";import * as React from "react";export function RunningAndDone() {    const [startedAt] = React.useState(() => Date.now() - 4000);    const steps = [        { label: "Pulling containment for the last 14 days" },        { label: "Splitting the drop by intent and transfer reason" },    ];    return (        <div className="flex w-full max-w-md flex-col gap-5">            <Thinking steps={steps} status="running" startedAt={startedAt} />            <Thinking steps={steps} status="done" elapsed={6} />        </div>    );}

Steps with details

Open, every step sits on the rail. A detail line carries the one fact the step turned up.

  1. Reading the current prompt and its 14 variables
  2. Comparing against last week's failed calls31 calls ended in a repeat loop, 12 were transferred after a date question.
  3. Checking which instructions the model skipped
  4. Drafting the smallest change that fixes both
Thought for 4 seconds
import { Thinking } from "@oration/canon/components/ai/thinking";export function WithDetails() {    return (        <div className="w-full max-w-md">            <Thinking                status="done"                elapsed={4}                defaultOpen                steps={[                    {                        label: "Reading the current prompt and its 14 variables",                    },                    {                        label: "Comparing against last week's failed calls",                        detail: "31 calls ended in a repeat loop, 12 were transferred after a date question.",                    },                    { label: "Checking which instructions the model skipped" },                    { label: "Drafting the smallest change that fixes both" },                ]}            />        </div>    );}

Before a retrieval answer

In a knowledge base test the trace starts open, with present-tense steps, then settles to past tense when the answer arrives. Keeping one instance keeps its open state.

Can a supplier change bank details over the phone?

  1. Embedding the question
  2. Searching 1,240 chunksHybrid search, keyword and vector
  3. Reranking the top 8 passages
Thinking
import { Thinking } from "@oration/canon/components/ai/thinking";import * as React from "react";export function Retrieval() {    const [startedAt] = React.useState(() => Date.now());    const [done, setDone] = React.useState(false);    React.useEffect(() => {        const id = window.setTimeout(() => setDone(true), 2400);        return () => window.clearTimeout(id);    }, []);    return (        <div className="flex w-full max-w-md flex-col gap-3">            <p className="self-end rounded-xl bg-muted px-3 py-2 text-13 text-foreground">                Can a supplier change bank details over the phone?            </p>            <Thinking                status={done ? "done" : "running"}                startedAt={startedAt}                defaultOpen                steps={                    done                        ? [                              { label: "Embedded the question" },                              { label: "Searched 1,240 chunks" },                              { label: "Kept 3 passages above 0.5" },                          ]                        : [                              { label: "Embedding the question" },                              {                                  label: "Searching 1,240 chunks",                                  detail: "Hybrid search, keyword and vector",                              },                              { label: "Reranking the top 8 passages" },                          ]                }            />            {done ? (                <p className="text-13 leading-5 text-pretty text-foreground">                    No. Bank changes need a verified callback to the number                    already on file, and the agent can't apply them during the                    call.                </p>            ) : null}        </div>    );}

Controlled

Pass open and onOpenChange to drive it from outside, for example from a preference to always show reasoning.

Always show reasoning
  1. Reading transferred calls from Thursday96 transfers, 81 of them mention a remittance.
  2. Checking tool errors in the same window
Thought for 3 seconds
import { Thinking } from "@oration/canon/components/ai/thinking";import { Switch } from "@oration/canon/components/switch";import * as React from "react";export function Controlled() {    const [open, setOpen] = React.useState(true);    return (        <div className="flex w-full max-w-md flex-col gap-4">            <Switch                label="Always show reasoning"                checked={open}                onCheckedChange={setOpen}            />            <Thinking                status="done"                elapsed={3}                open={open}                onOpenChange={setOpen}                steps={[                    {                        label: "Reading transferred calls from Thursday",                        detail: "96 transfers, 81 of them mention a remittance.",                    },                    { label: "Checking tool errors in the same window" },                ]}            />        </div>    );}

No steps

With an empty steps array it is a plain line with no chevron, for a model that reports only how long it thought.

Thought for 3s
Thought for 3 seconds
import { Thinking } from "@oration/canon/components/ai/thinking";export function NoSteps() {    return <Thinking status="done" elapsed={3} steps={[]} />;}

States#

Running, folded
Thinking
Running, open
  1. Reading the ticket
  2. Checking the payment
Thinking
Done, folded
Thought for 4 seconds
Done, open
  1. Reading the ticket
  2. Checking the payment
Thought for 4 seconds
Hover
Thought for 4 seconds
Focus visible
Thought for 4 seconds
import { Thinking } from "@oration/canon/components/ai/thinking";import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function StatesMatrix() {    const [startedAt] = React.useState(() => Date.now() - 4000);    const steps = [        { label: "Reading the ticket" },        { label: "Checking the payment" },    ];    const cells = [        { label: "Running, folded", running: true, open: false, className: "" },        { label: "Running, open", running: true, open: true, className: "" },        { label: "Done, folded", running: false, open: false, className: "" },        { label: "Done, open", running: false, open: true, className: "" },        {            label: "Hover",            running: false,            open: false,            className: "[&_[data-slot=collapsible-trigger]]:text-foreground",        },        {            label: "Focus visible",            running: false,            open: false,            className:                "[&_[data-slot=collapsible-trigger]]:ring-3 [&_[data-slot=collapsible-trigger]]:ring-ring/40",        },    ];    return (        <div className="grid w-full grid-cols-1 items-start gap-x-6 gap-y-5 sm:grid-cols-2">            {cells.map((cell) => (                <div key={cell.label} className="flex min-w-0 flex-col gap-2">                    <span className="text-xs text-muted-foreground">                        {cell.label}                    </span>                    <Thinking                        steps={steps}                        status={cell.running ? "running" : "done"}                        startedAt={startedAt}                        elapsed={cell.running ? undefined : 4}                        open={cell.open}                        className={cn("pointer-events-none", cell.className)}                    />                </div>            ))}        </div>    );}
States
StateTreatment
Running, foldedGrid rippling, Thinking… shimmering, seconds ticking, the latest step beside it.
Running, openSteps listed on the rail. New steps rise 4px and fade in on spring.moderate; the last one shimmers.
Done, foldedThought for 6s. The grid rests and the counter stops at the finishing time.
Done, openEvery step in plain text, details below.
HoverThe summary turns from Slate Meta to ink over 150ms.
Focus visibleA 3px Focus Indigo ring at 40% around the summary.
No stepsWith an empty steps array it is a plain line, not a button, with no chevron.
Reduced motionThe grid holds still, shimmer stops, steps appear without rising and the list opens without animating height.

Behavior#

  • It is a Base UI Collapsible. The summary is the trigger, with aria-expanded; Enter and Space toggle it.
  • Uncontrolled by default (defaultOpen). Pass open and onOpenChange to control it, for example from a preference.
  • The counter comes from useElapsed: whole seconds since startedAt, or since mount. It ticks while running and freezes when status turns done.
  • elapsed overrides the counter. Use it for traces that finished before this render, such as a turn loaded from history.
  • A done trace never reads Thought for 0s; the minimum is one second.
  • Steps already on screen at mount don't animate; only steps added later rise in.
  • The open list animates its height over 200ms through the shared collapsible styles.

Do and don't#

Thought for 4 seconds

The $412.50 difference is the 2% early-payment discount.

Do. Keep the trace folded above the answer, so the answer is the first thing people read.
  1. Reading the ticket
  2. Checking the payment in the ERP
  3. Matching the early-pay terms
Thought for 4 seconds

The $412.50 difference is the 2% early-payment discount.

Don't. Open the trace by default. The steps push the answer below the fold and read as noise.
  1. Reading 31 failed calls from last week
  2. Checking which instructions the model skipped
  3. Drafting the smallest change that fixes both
Thought for 5 seconds
Do. Write each step as a specific action with the object and, where it helps, a number: Reading 31 failed calls from last week.
  1. Analyzing
  2. Processing data
  3. Thinking deeply
Thought for 5 seconds
Don't. Write filler steps such as Analyzing, Processing data or Thinking deeply. They say nothing a loader doesn't.

Content#

  • Write steps as an -ing verb plus the object, without an ellipsis: Reading the ticket and 3 earlier messages, Checking the payment in the ERP.
  • When a trace loads already finished, write its steps in the past tense: Embedded the question, Kept 3 passages above 0.5.
  • Use detail for the one fact the step turned up: 31 calls ended in a repeat loop, 12 were transferred after a date question.
  • Three to five steps is enough. Merge steps that differ only in wording.
  • The summary words (Thinking…, Thought for) are fixed by the component.

Accessibility#

  • The summary is a <button> with aria-expanded. Its name includes the elapsed time and, while folded, the latest step.
  • A separate visually hidden role="status" says Thinking while running and Thought for 6 seconds when done, so the finish is announced once.
  • The ticking seconds and new steps are not announced one by one, which keeps screen readers quiet while it works.
  • Under reduced motion nothing moves; the words carry the state.
Keyboard interactions
KeysAction
TabMoves to the summary when there are steps.
EnterOpens or closes the steps.
SpaceOpens or closes the steps.

Design tokens#

Design tokens
TokenUsed for
--muted-foregroundSummary text at rest, latest step, details
--foregroundGrid cells, summary on hover, finished steps at 80%
--borderThe rail
--ringFocus ring at 40%
text-shimmerThinking… and the current step
spring.moderateEntrance of new steps
text-13Summary and step labels; details at text-xs

API reference#

Thinking

The trace. Also exported: the ThinkingStep type, { label: string; detail?: string }.

Other props spread onto Nothing. Only the props below are read..

Props of Thinking
PropTypeDefaultDescription
stepsRequiredThinkingStep[]No defaultSteps in order. The last one is current while running.
statusRequired"running" | "done"No defaultRunning ticks and shimmers; done settles to Thought for.
startedAtnumberNo defaultEpoch ms the reasoning began. Defaults to mount.
elapsednumberNo defaultSeconds. Overrides the live counter, for traces that finished earlier.
defaultOpenbooleanfalseInitial open state when uncontrolled.
openbooleanNo defaultControlled open state.
onOpenChange(open: boolean) => voidNo defaultCalled when the person toggles it.
classNamestringNo defaultMerged onto the root.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

elapsed isn't rounded. The knowledge base panel passes 2.2 and the summary reads Thought for 2.2s.

There is no stopped or failed status. When someone stops a Copilot turn mid-thought, the trace reads Thought for 3s as if it had finished.

The knowledge base panel swaps a running trace (open, present tense) for a new done one (folded, past tense), so the open state is lost at the switch. One instance with changing steps would keep it.

The summary words can't be changed, so a trace for retrieval still says Thinking… rather than Searching….

open and onOpenChange aren't used anywhere in the product yet.