Skip to content

ThinkingTree

Hero example

When to use

Reach for ThinkingTree when the agent is narrating what it is doing right now — checking compliance, reading a prior filing, drafting an artifact. The component answers "show me the API calls underneath the plan" without surfacing raw JSON. Canonical mounts are the marketing hero (under the Incorporation plan card), the in-app assistant chat (wrapped by LiveThinkingPanel), and the per-beat transcript renderer in the mock-founder story pipeline.

Don't reach for ThinkingTree when the user needs to choose a next step — that's a PlanCard (high-level checklist) or a SlidingPill (view switcher). Don't use it for historical audit events either — those carry actor and timestamp metadata and belong in the Timeline namespace.

Do — render ThinkingTree while an agent is executing, with rows accumulating as new actions land.
Don't — render ThinkingTree as a static checklist. The active row's shimmer implies live work; without motion the metaphor breaks.

Anatomy

Four structural parts:

  1. Header (carded mode only). Chevron + dual-mode counter — Thinking · step N of M while live, Thought for {duration} · {n} steps once final. Clicking the header toggles body collapse.
  2. Dot. A 14 px atom whose data-state attribute carries the recipe — green fill with a check (done), ink fill (active), hairline ring (queued), red fill with an ✕ (failed), dashed ring (skipped).
  3. Rail. Two 1 px hairline segments per row (upper + lower). Each inherits the state of its adjacent step: the upper segment from the step above, the lower from the current row. Completed segments darken to --status-green; failed segments darken to --status-red.
  4. Label. Sans text (--font-sans, Geist). Active rows sit inside a translucent glass pill with a continuous moving-gradient text shimmer; queued and skipped rows use --fg-soft; failed rows use --fg; done rows use --fg.

The row is a <li> inside <ol>. The active row carries aria-current="step".

Props

PropTypeDefaultDescription
itemsrequiredThinkingTreeItem[]
classNamestring
aria-labelstring
modeThinkingTreeModeWhen set, the tree renders in carded mode with a header. Omit to render only the row list (default, used by the marketing hero).
elapsedMsnumber0Total elapsed time for the turn, in ms. Used by the header.
collapsedbooleanControlled collapse state.
onToggle() => voidToggle handler. If omitted, the tree owns its own collapse state.
showTimingsbooleanfalseReveal per-row durations / `…` / `failed` / `skipped` chips.
bodyIdstringOptional ID hook so the wrapping recipe can wire `aria-controls`.
showHeaderbooleanForce header off even when `mode` is set. Used by recipes that provide their own header chrome.

items is the only required prop. Each ThinkingTreeItem is { id, state, label?, verb?, object?, durationMs? }. id is the React key — use the action's stable identifier (a tool-call id, a step index) so re-ordering doesn't remount rows mid-shimmer. Pass label for a single-line step description, or verb + object for the bold-leading-clause + muted-trailing-clause shape used by the marketing hero.

States

StateBulletLabelRail (below)
doneGreen fill, white ✓Verb --fg · object --fg-muted--status-green
activeInk fill, no glyphGlass pill + moving shimmer--ink-8 (default)
queuedHairline ringBoth --fg-soft--ink-8 (default)
failedRed fill, white ✕Verb --fg · object --fg-muted--status-red
skippedDashed ring--fg-soft + strikethrough--ink-8 (default)

The legacy state pending is silently mapped to queued for back-compat with the marketing hero — prefer queued in new code.

State transitions animate background, opacity, and border-color over 220 ms — a row promoting from queuedactivedone reads as one continuous motion.

Modes

mode is the primary axis:

  • bare (default). No header, no card chrome — just the <ol> of rows. Used by the marketing hero's sliding three-row window where vertical room is tight.
  • carded (mode="live" | "final"). Wraps the rows in a hairline card with a TraceHeader. live renders Thinking · step N of M, final renders Thought for X · N steps. Pass collapsed + onToggle for controlled collapse, or omit both to let the component own its state.

Density

Density-agnostic — row vertical padding is fixed at 11 px and the type scale is the canonical sans body. To show fewer or more rows, slice the items array at the consumer.

Themes

Tokens consumed

colors17 tokensv2.3.0
#3FBF6E
matter
src ↗
#1b7a47
matter
src ↗
#E8A845
matter
src ↗
#8a5e1c
matter
src ↗
#8C8C8C
matter
src ↗
#5A5A5A
matter
src ↗
#6E8AD8
matter
src ↗
#3a5cb8
matter
src ↗
#1f8a5b
brand
src ↗
rgba(31, 138, 91, 0.1)
brand
src ↗
rgba(31, 138, 91, 0.35)
brand
src ↗
#6b7280
brand
src ↗
rgba(107, 114, 128, 0.1)
brand
src ↗
rgba(107, 114, 128, 0.3)
brand
src ↗
#b0322b
brand
src ↗
rgba(176, 50, 43, 0.1)
brand
src ↗
rgba(176, 50, 43, 0.35)
brand
src ↗
colors6 tokensv2.3.0
#0D0D0D
matter
src ↗
#5A5A5A
matter
src ↗
#8C8C8C
matter
src ↗
#707070
matter
src ↗
#14110D
matter
src ↗
#627e68
brand
src ↗

Rails: --ink-8 (default), --status-green, --status-red. Dots: --status-green, --status-red, --fg, --fg-soft, --ink-15. Active pill: --paper-72, --paper-85, --paper-90. Labels: --fg, --fg-muted, --fg-soft. Font: --font-sans.

Accessibility

  • Keyboard. None in bare mode. In carded mode the header is a focusable <button>.
  • ARIA. Bare mode renders <ol aria-label>. Carded mode renders <div role="group" aria-label> with the rows as <ol id={controlsId}> and the header carrying aria-controls={controlsId} + aria-expanded.
  • Screen reader. Rows announce as "Verb Object" or as the single label string; the visual weight contrast and the state glyphs are purely decorative.
  • Reduced motion. The active row's moving-gradient shimmer collapses to a static --fg fill. The collapse transition still applies but is imperceptible at its duration.
  • Forced colors. Dots, rails, and the pill border collapse to CanvasText; labels inherit system foreground.

Do / Don't

Do — render concrete artifact names ("Certificate of Incorporation.pdf", "Delaware filing fees"). Specificity is the visual identity.
Don't — render generic verbs without objects ("Processing", "Working"). Verb-only rows degrade the tree to a spinner.
Do — slide a window of 3 rows in marketing surfaces where vertical room is tight. Older rows scroll off the top as new ones land.
Don't — render all 8+ rows in a small hero. The rail grows past the readable height and the active row drifts off-screen.

Recipes

  • Marketing hero — under the Incorporation plan card, bare mode, sliding three-row window. See apps/web/app/[locale]/(home)/components/chat-panel.tsx.
  • Live agent panelLiveThinkingPanel wraps ThinkingTree with mode="live" while streaming, mode="final" once the turn lands.
  • Mock-founder story — embedded in the per-beat transcript renderer; carded mode, no live shimmer (every row already settled).

Code example

import { ThinkingTree, type ThinkingTreeItem } from "@matter/components";

const items: ThinkingTreeItem[] = [
  { id: "1", label: "Checked compliance obligations",      state: "done"    },
  { id: "2", label: "Looked up authorized share count",    state: "done"    },
  { id: "3", label: "Read prior-year filing for par value", state: "failed" },
  { id: "4", label: "Calculating tax under both methods",  state: "active"  },
  { id: "5", label: "Auto-emailing the filing",            state: "skipped" },
  { id: "6", label: "Drafting the franchise tax filing",   state: "queued"  },
];

export function FilingActivity() {
  return (
    <ThinkingTree
      aria-label="Franchise-tax run"
      elapsedMs={4200}
      items={items}
      mode="live"
    />
  );
}

Source

packages/components/src/ThinkingTree

On this page