Steps
A collapsible timeline of a run — reasoning, tool calls, and answered questions as chronological steps.
Usage guidelines
- Recursive disclosure tree — every node is a
Steps.Itemwith aTriggerand aPanel, and panels can hold further items, so timelines nest arbitrarily. - Status-driven — each item's
status(complete/active/pending) flows to itsIconandLabelvia context; active items open by default. - Nesting — a nested item surfaces
data-nestedfor the indent rail; a static row is anIconand aLabelin 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.
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".
| Prop | Type | Default |
|---|---|---|
status | string | "complete" |
defaultOpen | boolean | status === active |
open | boolean | — |
onOpenChange | (open: boolean) => void | — |
| Attribute | Values | Details |
|---|---|---|
data-steps-item | — | |
data-status | string | |
data-nested | "true" | |
data-open | — | |
data-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.
| Prop | Type | Default |
|---|---|---|
keepMounted | boolean | false |
| Attribute | Values | Details |
|---|---|---|
data-steps-panel | — | |
data-open | — | |
data-closed | — | |
data-starting-style | — | |
data-ending-style | — | |
--panel-height | measured px |
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.
| Prop | Type | Default |
|---|---|---|
status | string | — |
| Attribute | Values | Details |
|---|---|---|
data-steps-icon | — | |
data-status | string |
Steps.Label
The row's text. Resolves status the same way the icon does, so one attribute
drives both. Renders a <span> element.
| Prop | Type | Default |
|---|---|---|
status | string | — |
| Attribute | Values | Details |
|---|---|---|
data-steps-label | — | |
data-status | string |
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.
| Prop | Type | Default |
|---|---|---|
status | string | — |
children | ReactNode | the resolved status string |
| Attribute | Values | Details |
|---|---|---|
data-steps-status |