Stepper shows sequential workflow progress as a vertical list of steps connected by a rail. Per the AGENTS.md decision table, use it for "sequential workflow progress" — not Tabs, which is for switching between views in one context rather than showing progression through steps.
Installation
HTML — already included if you loaded the 3 stylesheets — see the Installation guide.
React
npm install @operant/ui
@operant/ui is currently a monorepo workspace package, not yet published to the public npm registry.
Usage
HTML
<div class="ods-stepper" aria-label="Execution steps">
<div class="ods-step is-done">
<div class="ods-step-rail">
<div class="ods-step-node" aria-hidden="true"><!-- check icon --></div>
<div class="ods-step-line"></div>
</div>
<div class="ods-step-body">
<div class="ods-step-title">Detect</div>
<div class="ods-step-meta">alert ingested · 0.4s</div>
</div>
</div>
<div class="ods-step is-active" aria-current="step">
<div class="ods-step-rail">
<div class="ods-step-node" aria-hidden="true">2</div>
</div>
<div class="ods-step-body">
<div class="ods-step-title">Diagnose</div>
<div class="ods-step-meta">4 tools · running…</div>
</div>
</div>
</div>
React
import { Stepper, Step } from "@operant/ui";
export default function Example() {
return (
<Stepper aria-label="Execution steps">
<Step status="done" title="Detect">alert ingested · 0.4s</Step>
<Step status="active" title="Diagnose">4 tools · running…</Step>
</Stepper>
);
}
Examples
Full run flow (all four states)
One step in each of the four states: done (check icon, success color), active (numbered node, agent-active/iris color, aria-current="step"), error (X icon, danger color), and pending (plain numbered node — there is no dedicated "pending" modifier class; the unstyled .ods-step node IS the pending look). Note the last step in the list has no trailing .ods-step-line — Stepper omits the connector after the final child automatically.
HTML
<div class="ods-stepper" aria-label="Execution steps">
<div class="ods-step is-done">
<div class="ods-step-rail">
<div class="ods-step-node" aria-hidden="true"><svg width="12" height="12"><!-- check --></svg></div>
<div class="ods-step-line"></div>
</div>
<div class="ods-step-body">
<div class="ods-step-title">Detect</div>
<div class="ods-step-meta">alert ingested · 0.4s</div>
</div>
</div>
<div class="ods-step is-active" aria-current="step">
<div class="ods-step-rail">
<div class="ods-step-node" aria-hidden="true">2</div>
<div class="ods-step-line"></div>
</div>
<div class="ods-step-body">
<div class="ods-step-title">Diagnose</div>
<div class="ods-step-meta">4 tools · running…</div>
</div>
</div>
<div class="ods-step is-error">
<div class="ods-step-rail">
<div class="ods-step-node" aria-hidden="true"><svg width="12" height="12"><!-- x --></svg></div>
<div class="ods-step-line"></div>
</div>
<div class="ods-step-body">
<div class="ods-step-title">Remediate</div>
<div class="ods-step-meta">tool call `deploy` failed</div>
</div>
</div>
<div class="ods-step">
<div class="ods-step-rail">
<div class="ods-step-node" aria-hidden="true">4</div>
</div>
<div class="ods-step-body">
<div class="ods-step-title">Verify</div>
</div>
</div>
</div>
React
<Stepper aria-label="Execution steps">
<Step status="done" title="Detect">alert ingested · 0.4s</Step>
<Step status="active" title="Diagnose">4 tools · running…</Step>
<Step status="error" title="Remediate">tool call `deploy` failed</Step>
<Step status="pending" title="Verify" />
</Stepper>
Stepper automatically computes each Step's 1-based index (shown inside the node for pending/active steps) and marks the last child isLast so its rail omits the trailing line — you never pass index/isLast yourself unless rendering a Step outside a Stepper.
API Reference
<Stepper>
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Required. Step elements; index/isLast are injected automatically based on list position. |
className | string | — | Merged with .ods-stepper. |
<Step>
| Prop | Type | Default | Description |
|---|---|---|---|
status | "pending" | "active" | "done" | "error" | — | Required. Drives the node's modifier class, icon, and color. |
title | string | — | Required. Rendered in .ods-step-title. |
children | ReactNode | — | Supporting detail rendered under the title in .ods-step-body (timing, tool count, error reason). |
index | number | — | 1-based position shown inside the node for pending/active steps. Set automatically by Stepper; only pass explicitly outside one. |
isLast | boolean | — | Omits the trailing rail connector. Set automatically by Stepper. |
className | string | — | Merged with .ods-step. |
status → modifier mapping: done → .is-done (success color, check icon), active → .is-active (agent-active/iris color, numbered node, aria-current="step"), error → .is-error (danger color, X icon), pending → no modifier class (plain numbered node).
HTML-side classes: .ods-stepper (vertical flex container), .ods-step (a single row) + state modifiers .is-done / .is-active / .is-error (no class for pending — that's the base look), .ods-step-rail (icon column), .ods-step-node (22px circle), .ods-step-line (connector to the next step), .ods-step-body, .ods-step-title, .ods-step-meta (secondary detail text). Note: css/components.css also defines an .is-review modifier (human-review/amber color) not currently exposed as a StepStatus value in the React component — use it directly in HTML if you need a "needs human review" step state.
Accessibility
- The active step carries
aria-current="step", giving assistive tech an explicit marker for "where the user/agent currently is" beyond the visual color change. - Status icons (check/X) are
aria-hidden="true"— the step's status is conveyed through the adjacent text title/meta, not decoded from the icon alone (AGENTS.md#4/#8: color/icon is never the only signal). - Give the
Steppercontainer anaria-label(oraria-labelledby) describing what the sequence represents, e.g."Execution steps". - The rail/line connectors are
aria-hiddenby omission (plain<div>s, not announced) — only the title and meta text carry semantic content.