Stepper
A bounded linear process as an ordered list — seven step states on a numbered rail that fills in behind you, with focus that follows the flow.
- Status
- Since
0.4.0- Accessibility pattern
- ordered list with aria-current
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Stepper before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify stepperInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/stepperRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/progress.
Usage
import { Stepper } from "@/components/ui/stepper";
<Stepper
aria-label="Import"
steps={[
{ id: "upload", label: "Upload file", state: "complete" },
{ id: "map", label: "Map columns", state: "current" },
{ id: "review", label: "Review", state: "upcoming" },
]}
/>;Not Tabs. role="tab" announces "tab 2 of 6"
and implies free navigation between peers — actively misleading for a flow
where step 4 is unreachable until step 3 validates. Stepper is an ordered
list with aria-current="step", which is what a wizard actually is. Where a
full step list is too heavy, use Progress
directly — though below collapse this component already becomes one.
It has no Back and Next, and most uses need none. Reach for Stepper
alone when the process is driven by the system and read by a person — an order
or delivery tracker, a deployment pipeline, an application's status, a long
import running on a server. When a person drives the flow and it needs
controls, guards and step bodies, use
MultiStepForm, which composes this
component and owns the action row.
Scope
It communicates the process, and nothing else.
| Behaviour | Where it lives |
|---|---|
| Back/Next controls | MultiStepForm, or your own buttons |
| Step bodies and validation | MultiStepForm, or your own forms (Field + a validator) |
| Why the flow will not advance | Beside the control it blocks, never in the rail |
| Deriving states from an index | Host — states are explicit so failed/skipped steps stay expressible |
The refusal message is the one to read twice. Stepper deliberately does not
render it: the only place it could sit is under the current step's label, which
in a horizontal rail is a one-column-wide ribbon and in the collapsed form does
not exist. A reason belongs next to the button it refuses.
Anatomy
Examples
When the host drives it
The buttons below belong to the page, not to Stepper — it renders none. They
sit right-aligned with no divider, Back on variant="secondary", which is how
MultiStepForm draws its action row too. For anything more than a demo, use that component rather than wiring this
by hand.
Every state
Seven states, and the two that are coloured are coloured because something needs
the user. current alone takes the solid primary fill and a medium-weight label;
complete is the same hue muted — a check on a light primary tint, with the
connector behind it in the same tone — and upcoming is an outline with a muted
label. Progress is not success: a rail of green leaves nothing louder for the
step that has actually failed. loading is the state an async advance gate occupies while it
runs, and skipped records a step passed over rather than one that failed.
- Upload fileCompletedcustomers-2026.csv
- 2Map columnsCurrent step12 of 12 matched
- Check for duplicatesCheckingScanning 8,400 rows
- Review changesNeeds review40 rows will be overwritten
- Resolve conflictsNeeds attention3 rows need a decision
- 6Notify ownersOptionalSkippedPassed over
- 7ImportNot started
Error state
The state every hand-rolled stepper forgets: a step that failed after
completion. Its node takes the destructive fill, its label the family's
-text ink, and the rail behind it does not fill — you have not passed it.
When the step that failed is the one the user is on, set current: true
alongside state: 'error': it keeps aria-current="step" and the step count
while showing the error (MultiStepForm does this for you).
Collapsing to a summary
Below collapse the rail becomes the current step's name, its position, and a
Progress bar. It is a container query, not a viewport one, so a rail
inside a narrow dialog collapses on a wide screen too. Both examples below are
the same component with the same props; only the box around them differs.
Sizes
Inline labels
labelPosition="inline" sets each label beside its node with the connector
running on from it — a compact rail for a card header. The DOM and the reading
order are identical to the default.
Vertical and navigable
orientation="auto" (the default) switches to vertical at verticalFrom steps,
because a rail long enough to crush its own labels reads better down the page.
In navigable mode, completed, skipped and error steps become real buttons
for revisiting — the whole step, node and label together, is one target, so the
circle is as clickable as the text — a failed step is exactly the one the user needs to get back to;
current, upcoming, and disabled steps never do.
- 3ReviewCurrent step
- 4ImportNot started
A rail wider than its container
Every step keeps a readable minimum width, so six steps in a narrow container scroll sideways
instead of crushing their labels (pass collapse={false} to keep the rail at every width), and
the list scrolls the current step into view — on first render too — without moving the page.
Revisitable and current steps share one step box — same padding — and the current label's bold
width is reserved, so the rail never shifts as the current step moves. A revisitable step shows
only the standard hover fill across its node and label, with no press motion.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
steps* | StepperStep[] | — | The ordered steps. Exactly one should carry state: "current" or "loading". |
aria-label | string | 'Progress' | Accessible name for the process. |
collapse | false | StepperCollapse | 'auto' | Container width below which the rail is replaced by a compact summary — the current
step's label, its position, and a Progress bar. A container query, not a viewport
one, so a rail inside a narrow dialog collapses on a wide screen too. auto derives
the breakpoint from the step count; a named breakpoint pins it. Applies to horizontal
orientation only — a vertical rail already reads correctly when narrow. Pass false
to keep the full rail at every width. |
labelPosition | "below" | "inline" | 'below' | Where the label sits relative to its node, in horizontal orientation only.
below gives each step a full-width column under its node; inline sets the label
beside the node with the connector running on from it, for a compact rail in a card
header. Ignored when vertical. |
navigable | boolean | false | Navigable mode: completed, skipped and error steps render as real buttons that fire
onStepSelect. In linear mode (the default) no step is interactive — movement belongs
to the host's Back/Next controls, and non-interactive steps are a promise that order
matters. |
onStepSelect | ((id: string) => void) | — | Fired in navigable mode when a revisitable step is activated. |
orientation | "auto" | "horizontal" | "vertical" | 'auto' | Layout direction. auto reads the step count: a rail long enough to crush its own
labels side by side is better read down the page, so it flips to vertical at
verticalFrom steps. Both orientations keep the same DOM and reading order. |
showCount | boolean | false | Renders a "Step n of N" line above the rail. The compact summary always shows it, so this only affects the full rail. |
size | "default" | "sm" | 'default' | Node and label scale together — default is upstream's tier name, sm is the compact
rail for a card header or a toolbar. |
verticalFrom | number | 6 | How many steps make orientation="auto" choose vertical. Counted from steps, so a
conditional step appearing mid-flow can flip the layout by itself. |
Data attributes and CSS variables on Stepper
| Attribute | Values |
|---|---|
data-disabled | "" |
data-label-position | mirrors a prop or state value |
data-orientation | mirrors a prop or state value |
data-passed | "" |
data-size | mirrors a prop or state value |
data-slot | "stepper" | "stepper-connector" | "stepper-content" | "stepper-count" | "stepper-description" | "stepper-item" | "stepper-label" | "stepper-list" | "stepper-optional" | "stepper-step" | "stepper-summary" | "stepper-trigger" |
data-state | mirrors a prop or state value |
Step
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | Stable identifier — selection events return it. |
label* | string | — | Visible step label. Receives focus when the step becomes current. |
state* | StepperStepState | — | The step's state. Exactly one step should be current or loading. |
current | boolean | false | Marks the step as the current one while its state says something else — the step
the user is on has failed validation (error) or carries a caveat (warning). It
keeps aria-current="step", the step count and focus-follow on that step, so a
failed step never stops being "where am I". |
description | string | — | Optional secondary line under the label. |
disabled | boolean | false | Marks a step unreachable even in navigable mode (e.g. gated by a plan). |
optional | boolean | false | Marks the step as one the flow may pass over, rendering an "Optional" affix. It is a
label, not a state: an optional step that was passed over is skipped. |
Accessibility
- An
<ol>labelled byaria-label, witharia-current="step"on the current item — never tab semantics. Aloadingstep is still the current step. - Step state is carried by the node's glyph and visually hidden text ("Completed", "Checking", "Needs review", "Needs attention", "Skipped"), never colour alone.
- When the current step changes, focus moves to the new step — not the first form field, and never on initial mount. Below the collapse width the rail is not laid out, so focus lands on the summary instead.
- The compact summary is a real
progressbar, named by the current step. - A navigable step is one
Buttonwrapping its node and label, named by the label and its state, so it clears the 24px touch floor and takes the system's single focus outline. The connector is decorative and never part of the target.
| Key | Action |
|---|---|
| Tab | Reach completed, skipped and error steps (navigable mode only). |
| Enter / Space | Revisit the focused step. |
| Contract | States tested |
|---|---|
| Behaviour | complete, current, loading, warning, error, skipped, upcoming, disabled, navigable, collapsed |
| Accessibility | keyboard, labeled, semantic-html, focus-management |
| Visual | complete, current, loading, warning, error, skipped, upcoming, disabled, hover |