Stepper
A bounded linear process as an ordered list — complete/current/upcoming/error states, aria-current="step", advance gating, and focus that follows the process.
- Status
- Since
0.4.0- Accessibility pattern
- ordered list with aria-current
Last updated
- Upload fileCompleted
- Map columnsCurrent step
- ReviewNot started
- ImportNot started
Install
Add Stepper from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/stepperThe same command installs the registry items it composes: @vegastack/button, @vegastack/status-icon.
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
ProgressIndicator's segments.
Scope
| Behaviour | Where it lives |
|---|---|
| Back/Next controls | Host Buttons — gating is the host's logic |
| Step bodies and validation | Host forms (Field + validation); see the multi-step form guide |
| Deriving states from an index | Host — states are explicit so failed/skipped steps stay expressible |
Examples
Error state
The state every hand-rolled stepper forgets: a step that failed after
completion. error is first-class — icon, tone, and text.
- Upload fileCompleted
- Map columnsNeeds attention2 columns unmapped
- ReviewCompleted
- ImportCurrent step
Advance gating
blockedReason communicates why the process cannot advance: rendered against
the current step, announced politely, and wireable to your Next button through
aria-describedby.
- Upload fileCompleted
- Map columnsCurrent stepMap every required column to continue
- ReviewNot started
- ImportNot started
Vertical and navigable
Vertical keeps the identical DOM and reading order. In navigable mode,
completed steps become real buttons for revisiting; current, upcoming, and
disabled steps never do.
- CSV or XLSX, up to 10 MB
- ReviewCurrent step
- ImportNot started
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
steps* | StepperStep[] | — | The ordered steps. Exactly one should carry state: "current". |
aria-label | string | "Progress" | Accessible name for the process. |
blockedReason | string | — | Why the process cannot advance right now ("Select at least one column"). Rendered against the current step and announced politely. Gating itself is the host's logic — this communicates the block. |
blockedReasonId | string | — | Id for the blocked-reason element, so the host can wire
aria-describedby={blockedReasonId} on its own Next button. Auto-generated
when omitted. |
navigable | boolean | false | Navigable mode: completed 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. |
onStepSelect | ((id: string) => void) | — | Fired in navigable mode when a completed step is activated. |
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction. Both orientations keep the same DOM and reading order. |
Data attributes and CSS variables on Stepper
| Attribute | Values |
|---|---|
data-disabled | "" |
data-orientation | mirrors a prop or state value |
data-slot | "stepper" | "stepper-blocked-reason" | "stepper-connector" | "stepper-content" | "stepper-description" | "stepper-label" | "stepper-node" | "stepper-step" |
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. |
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). |
Accessibility
- An
<ol>labelled byaria-label, witharia-current="step"on the current item — never tab semantics. - Step state is carried by the icon shape and visually hidden text ("Completed", "Current step", "Not started", "Needs attention"), never colour alone.
- When the current step changes, focus moves to the new step's label — not the first form field, and never on initial mount.
- The blocked reason is a polite live region; give your Next button
aria-describedby={blockedReasonId}so the reason reads with the control.
| Key | Action |
|---|---|
| Tab | Reach completed steps (navigable mode only). |
| Enter / Space | Revisit the focused completed step. |
| Contract | States tested |
|---|---|
| Behaviour | complete, current, upcoming, error, disabled, gated, navigable |
| Accessibility | keyboard, labeled, status-announcement, semantic-html |
| Visual | complete, current, upcoming, error, disabled |