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 an Icon and a 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.
  • No composite keyboard model — each trigger is a real button, so the tree is plain sequential tab order with no roving focus to learn.
  • Panels animate from a published height — see Why the panel releases its height, which is also why an open panel keeps growing.
  • 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>

Examples

Driving rows from status

status is an opaque string. The package resolves it — own prop, then inherited from the enclosing item, then "complete" — and reflects it as data-status. It never decides what the set is, so the "error" below is a string this demo invented and then styled.

Read the requestcomplete
Searched the webcomplete
Checked the cacheerror
Wrote the answercomplete

Why the panel releases its height

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

API reference

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

Steps

The timeline container. Ships the disclosure and status plumbing and no row content: what a tool call or a reasoning step looks like is yours. Renders a <div> element.

Steps.Item

One node of the tree, and the unit that nests: an item's panel may hold further items, so a timeline goes as deep as the run did. Renders a <div> element, 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

The row that expands an item. A real button, so the tree is plain sequential tab order rather than a composite widget with its own keyboard model. Renders a <button> element (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 collapsible body. Publishes its measured height while a transition runs and releases it once settled open, so an open panel grows with content that arrives inside it. 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

The row's status glyph, aria-hidden because colour and shape announce nothing. Resolves status from its own prop, then the enclosing item. Renders a <span> element.

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

Steps.Label

The row's text. Resolves status the same way the icon does, so one attribute drives both. Renders a <span> element.

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

Steps.Status

The only part that speaks the status. Renders a visually hidden <span> saying the resolved status string; pass children to localise the wording. Carries 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.