Skip to content

Stepper

Step-by-step progress indicator with expandable steps, substeps, and five visual variants. Compound component with context-based state management, roving tabIndex keyboard model, and a polite live region for active-step announcements.

Preview

Installation

$pnpm exec dgadd add ui/stepper
[Installs to]src/components/ui/stepper[Item]ui/stepper

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Usage

tsx
import { Stepper } from "@/components/ui/stepper";export default function StepperDefault() {  return (    <Stepper>      <Stepper.Step stepId="select" status="completed">        <Stepper.Trigger>Select files for review</Stepper.Trigger>      </Stepper.Step>      <Stepper.Step stepId="analyze" status="active">        <Stepper.Trigger>Analyze code changes</Stepper.Trigger>      </Stepper.Step>      <Stepper.Step stepId="report" status="pending">        <Stepper.Trigger>Generate review report</Stepper.Trigger>      </Stepper.Step>    </Stepper>  );}

Vertical variants

Five visual variants share the same compound API and render the same six canonical states (pending · active · completed · error · skipped · disabled). Switch with variant on the root <Stepper>.

Preview
  • ascii. Default. Mono 1ch bracket glyphs ([x] [~] [ ] [!]) with a blinking cursor on the active step. Pure terminal idiom — matches dialog brackets, code-block chrome, and CLI output.
  • numbered. 20px square with the CSS-counter step number; swaps to on completed. Use for form wizards and onboarding where the step number matters to the user.
  • bullet. Single Unicode glyph per state ( · · · · ×) with a dashed connector. Minimal chrome — use when the stepper is one of many UI elements.
  • tag. Uppercase text tag (DONE · RUN · WAIT · FAIL · SKIP · OFF) with a fixed min-width so the active state never shifts neighbour widths. Use for CI / build / deploy surfaces where users read status as text.
  • progress. Unicode block-element bar (███ / █▌░ / ░░░) per step. Strong terminal flavor; best for short lists (3–5 steps) where granular per-step progress matters.

Six canonical states

Preview

For compact CI bars, wizard headers, and breadcrumb-style paths, use the separate Horizontal Stepper primitive.

More examples

Auto tone

Preview

With substeps

Preview

Controlled expansion

Preview

Error recovery

Preview

Keyboard navigation

Preview

API Reference

Stepper

NameTypeDefaultDescription
variant"ascii" | "numbered" | "bullet" | "tag" | "progress""ascii"Visual variant. Controls the indicator glyph and connector treatment across every step.
expandedIdsstring[]Controlled set of currently expanded step ids.
defaultExpandedIdsstring[]Initial expanded ids for uncontrolled mode.
onExpandedChange(ids: string[]) => voidFired when the expanded set changes.
childrenrequiredReactNodeStepperStep children rendered inside an <ol>.

StepperStep

NameTypeDefaultDescription
stepIdrequiredstringStable identifier matched against expandedIds.
statusrequired"pending" | "active" | "completed" | "error" | "skipped" | "disabled"Step status. Drives the indicator glyph, label styling, aria-current, aria-disabled, and tab-order eligibility.
childrenrequiredReactNodeStepperTrigger and optional StepperContent.

StepperTrigger

NameTypeDefaultDescription
statusLabelsPartial<Record<"pending" | "active" | "completed" | "error" | "skipped" | "disabled", string>>{ completed: "DONE", active: "RUN", pending: "WAIT", error: "FAIL", skipped: "SKIP", disabled: "OFF" }Per-status indicator label overrides. Used directly by `variant="tag"`; other variants use these labels as the screen-reader fallback for the indicator glyph.
indicatorClassNamestringClasses merged onto the indicator span, for repainting it from an app-owned palette.
labelClassNamestringClasses merged onto the label span. Same seam as indicatorClassName.
childrenrequiredReactNodeStep label rendered next to the indicator glyph.

StepperContent

NameTypeDefaultDescription
childrenrequiredReactNodeExpandable content (e.g. nested StepperSubstep rows). aria-hidden and inert when collapsed.
regionbooleanfalseOpt in to role="region" with aria-labelledby pointing at the trigger while the step is expanded. Off by default, matching AccordionContent and the APG disclosure pattern: a Stepper can hold any number of simultaneously expanded steps, and one landmark per step is rotor noise. Enable it for a small number of substantive panels.

StepperSubstep

NameTypeDefaultDescription
tagrequiredstringShort tag (e.g. step number or code) rendered inside a status-colored Badge.
labelrequiredstringAccessible label text for the substep.
statusrequired"pending" | "active" | "completed" | "error"Current substep status. Substeps keep the original four-state lifecycle (skipped/disabled apply only to top-level steps).
detailstringTrailing detail text. Overrides the status label fallback.
statusLabelsPartial<Record<"pending" | "active" | "completed" | "error", string>>Per-status fallback labels shown when detail is omitted.

Data attributes

AttributeApplies toValuesDescription
data-stateStepperStep"open" | "closed"Expanded/collapsed state for the step content.
data-statusStepperStep / StepperTrigger"pending" | "active" | "completed" | "error" | "skipped" | "disabled"Canonical step lifecycle state used by indicators and labels.
data-step-idStepperTriggerstep idStable id used by roving focus, expansion lookup, and trigger/content linking.
data-variantStepper"ascii" | "numbered" | "bullet" | "tag" | "progress"Visual indicator and connector variant.
data-counterStepperTriggerpresent in numbered variantCSS-counter hook for numbered indicators.

Accessibility

Keyboard Navigation

Roving tabIndex: a single Tab key reaches the stepper. Arrow keys (Up/Down/Left/Right) move focus between enabled steps and skip disabled ones. Home/End jump to the first/last enabled step. Enter or Space toggles direct StepperContent.

KeyAction
ArrowUp / ArrowLeftMoves focus to the previous enabled StepperTrigger.
ArrowDown / ArrowRightMoves focus to the next enabled StepperTrigger.
Home / EndMoves focus to the first or last enabled trigger.
Enter / SpaceToggles the focused step content when present.

Controlled expansion with keyboard

Preview

Keyboard navigation

Preview

Notes

Visual variants (vertical)

Five variants: ascii (default — mono 1ch bracket glyphs with blinking active cursor), numbered (CSS-counter square with ✓ on completed), bullet (single glyph with dashed connector), tag (uppercase text tag, no width shift on active), progress (Unicode block-element bar per step). All variants render the same six canonical states.

Six canonical states

pending · active · completed · error · skipped · disabled. `skipped` ≠ `completed` (data integrity for form wizards). `disabled` ≠ `pending` (policy gate vs ordering). Skipped renders line-through. Disabled is non-interactive and skipped by arrow-key navigation.

State color

Completed indicators, labels, and connectors read the primary token, not success: finishing a step is neutral progress, so it stays monochrome in both themes. The status palette stays reserved for meaning — error is the only status that keeps a semantic hue.

Repainting from an app palette

An app that owns its own status palette (e.g. live-run telemetry colors) passes `indicatorClassName`/`labelClassName` on StepperTrigger instead of targeting the trigger's inner spans with descendant selectors. Both merge over the variant classes, so a single utility wins the conflict without copying the rest of the state styling.

Substep nesting

StepperContent stacks below its trigger and indents its children. Substeps draw a terminal branch rail from that indent (├─ per row, └─ on the last), so nested rows read as children of the step rather than a second column.

Keyboard model

Single roving tab stop on the active step (falls back to the first non-disabled step). Arrow keys (Up/Down/Left/Right) cycle focus and skip disabled steps. Home/End jump to first/last enabled step. Editable targets inside step content keep their native handling. The focused trigger shows the library's outside focus ring (2px --ring, 2px offset) around the whole row.

Expansion modes

Supports controlled (expandedIds + onExpandedChange) and uncontrolled (defaultExpandedIds) expansion. Multiple steps can be expanded simultaneously.

Composition contract

Keep StepperTrigger and StepperContent as explicit children of StepperStep. StepperStep only links aria-controls when it can see a direct StepperContent child; opaque wrappers that create content internally are not part of the current public contract.

Live region

Active-step transitions are announced via a persistent polite live region: 'Step {n} of {total}: {label}'. Label is sourced from the trigger's text content.

Source

Install via CLI: pnpm exec dgadd add ui/stepper. Keyboard hooks are included as standalone copies. For the full experience, use --integration keys.

Highlighted source loads after this disclosure opens. Browse the source repository.