Steps

A collapsible timeline of a run — reasoning, tool calls, and answered questions as chronological steps.

Read the request
Writing the answer

Usage guidelines

  • Recursive disclosure tree — every node is a Steps.Item with a Trigger and a Panel, and panels can hold further items, so timelines nest arbitrarily.
  • Status-driven — each item's status (complete / active / pending) flows to its Icon and Label via context; active items open by default.
  • Nesting — a nested item surfaces data-nested for the indent rail; a static row is just an Icon + Label in a <div>.
  • You compose the rows — the primitive ships the disclosure + status plumbing; row content (icons, tool-call summaries) is yours to render.
  • Get started — see Quick start to add the package.

Anatomy

A timeline is a top-level item whose panel holds rows; a row is an Icon + Label, and a row that expands is itself a nested Steps.Item:

<Steps.Root>
  <Steps.Item defaultOpen>
    <Steps.Trigger>
      <span>Worked for 3 seconds</span>
    </Steps.Trigger>
    <Steps.Panel>
      {/* a static, complete row */}
      <div>
        <Steps.Icon>{checkIcon}</Steps.Icon>
        <Steps.Label>Read the request</Steps.Label>
      </div>

      {/* a nested, expandable row */}
      <Steps.Item defaultOpen>
        <Steps.Trigger>
          <Steps.Icon>{checkIcon}</Steps.Icon>
          <Steps.Label>Searched the web</Steps.Label>
        </Steps.Trigger>
        <Steps.Panel>Found three relevant sources and skimmed each.</Steps.Panel>
      </Steps.Item>

      {/* an in-progress row — status overrides icon + label styling */}
      <div>
        <Steps.Icon status="active">{spinnerIcon}</Steps.Icon>
        <Steps.Label status="active">Writing the answer</Steps.Label>
      </div>
    </Steps.Panel>
  </Steps.Item>
</Steps.Root>

Status

status is an opaque string — you own the set (commonly "complete", "active", "pending", but add "error"/"skipped"/anything). Steps.Item takes a status and publishes it through context; Steps.Icon and Steps.Label inherit it, or override with their own status. Resolution is own prop → inherited → "complete". Every status-aware part reflects it as data-status for styling.

Height transitions

The panel publishes its measured height as --panel-height while an open or close transition runs, and releases it once the panel settles open. So height: var(--panel-height) animates from a real number, and then — with the variable no longer written — becomes invalid at computed-value time and falls back to auto. That is what lets an open panel track content appearing inside it, rather than staying pinned to the height it had when it opened.

The demo at the top of this page uses it. Collapse and expand the timeline to see the transition, then expand Searched the web while the timeline is already open — the outer panel grows to fit the detail instead of clipping it.

Two details in that demo are load-bearing. data-starting-style and data-ending-style clamp the height to 0 on the transitional frames, and they outrank the base height because a data-attribute variant is more specific. And [&>*]:shrink-0 guards the measurement: a flex column clamped to height: 0 puts every child under shrink pressure, and a child that collapses to nothing makes the panel measure itself as 0px.

Keyboard

Each item is a standard disclosure: Steps.Trigger is a real button, so Tab moves between triggers and Enter/Space toggle the nearest item. Nested items nest their own triggers — there is no roving focus or composite widget; the tree is plain sequential tab order.

Accessibility

Disclosure semantics are inherited from the underlying Collapsible: aria-expanded/aria-controls on the trigger, an id-linked, hidden-managed panel. The item whose status is "active" additionally carries aria-current="step" — the same "active" convention defaultOpen already keys off.

Status itself is invisible to assistive tech by default: Steps.Icon is aria-hidden and color never announces. Mount Steps.Status inside rows whose status matters — it renders a visually-hidden span speaking the resolved status string, and takes children for localized copy:

<Steps.Trigger>
  <Steps.Icon />
  <Steps.Label>Searching the web</Steps.Label>
  <Steps.Status />
</Steps.Trigger>

API reference

Every part accepts className, style, and render (see Styling) and emits a bespoke part attribute (data-<part>) unless noted.

Steps

The timeline root. Renders data-steps. No part-specific props.

Steps.Item

One node of the tree (a disclosure). Renders data-steps-item, plus aria-current="step" while status is "active".

PropTypeDefault
statusstring
"complete"
defaultOpenboolean
status === active
openboolean
onOpenChange(open: boolean) => void
AttributeValuesDescription
data-steps-itemThe item element.
data-statusstringThe item's status (commonly complete / active / pending).
data-nested"true"Present when the item is inside another item (indent rail).
data-openPresent while open.
data-closedPresent while closed.

Steps.Trigger

Toggles the nearest item. Renders a <button data-steps-trigger> (aria-expanded, aria-controls). Carries data-open/data-closed for the chevron. The styled layer groups it as group/steps-trigger so children read group-data-open/steps-trigger:….

Steps.Panel

The nearest item's disclosure area — lays out the timeline column. Renders data-steps-panel.

PropTypeDefault
keepMountedboolean
false
AttributeValuesDescription
data-steps-panelThe panel.
data-openPresent while open.
data-closedPresent while closed.
data-starting-stylePresent on the first open frame (enter transition).
data-ending-stylePresent while the exit animation runs.
--panel-heightmeasured pxThe panel's natural height, published only while the open or close transition runs so a height transition has a number to animate from. Deliberately released once the panel settles open, which makes `height: var(--panel-height)` fall back to `auto` so the open panel tracks content that grows inside it.

Steps.Icon

Status indicator. Renders <span data-steps-icon aria-hidden>.

PropTypeDefault
statusstring
AttributeValuesDescription
data-steps-iconThe icon element.
data-statusstringResolved status, for styling.

Steps.Label

Row text. Renders <span data-steps-label>.

PropTypeDefault
statusstring
AttributeValuesDescription
data-steps-labelThe label element.
data-statusstringResolved status, for styling.

Steps.Status

Visually-hidden status announcement. Renders <span data-steps-status> with screen-reader-only styling (overridable via style/className), containing the resolved status string unless children provide localized copy.

PropTypeDefault
statusstring
childrenReactNode
the resolved status string
AttributeDescription
data-steps-statusThe status element.