Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

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
stable
Since
0.4.0
Accessibility pattern
ordered list with aria-current

Last updated

  1. Upload fileCompleted
  2. Map columnsCurrent step
  3. ReviewNot started
  4. 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/stepper

The 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

BehaviourWhere it lives
Back/Next controlsHost Buttons — gating is the host's logic
Step bodies and validationHost forms (Field + validation); see the multi-step form guide
Deriving states from an indexHost — 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.

  1. Upload fileCompleted
  2. Map columnsNeeds attention2 columns unmapped
  3. ReviewCompleted
  4. 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.

  1. Upload fileCompleted
  2. Map columnsCurrent stepMap every required column to continue
  3. ReviewNot started
  4. 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.

  1. CSV or XLSX, up to 10 MB
  2. ReviewCurrent step
  3. ImportNot started

API Reference

PropTypeDefaultDescription
steps*StepperStep[]The ordered steps. Exactly one should carry state: "current".
aria-labelstring"Progress"Accessible name for the process.
blockedReasonstringWhy 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.
blockedReasonIdstringId for the blocked-reason element, so the host can wire aria-describedby={blockedReasonId} on its own Next button. Auto-generated when omitted.
navigablebooleanfalseNavigable 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

AttributeValues
data-disabled""
data-orientationmirrors a prop or state value
data-slot"stepper" | "stepper-blocked-reason" | "stepper-connector" | "stepper-content" | "stepper-description" | "stepper-label" | "stepper-node" | "stepper-step"
data-statemirrors a prop or state value

Step

PropTypeDefaultDescription
id*stringStable identifier — selection events return it.
label*stringVisible step label. Receives focus when the step becomes current.
state*StepperStepStateThe step's state. Exactly one step should be current.
descriptionstringOptional secondary line under the label.
disabledbooleanfalseMarks a step unreachable even in navigable mode (e.g. gated by a plan).

Accessibility

  • An <ol> labelled by aria-label, with aria-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.
KeyAction
TabReach completed steps (navigable mode only).
Enter / SpaceRevisit the focused completed step.
ContractStates tested
Behaviourcomplete, current, upcoming, error, disabled, gated, navigable
Accessibilitykeyboard, labeled, status-announcement, semantic-html
Visualcomplete, current, upcoming, error, disabled

Do / Don't

Do
Model a failed step as state: 'error' with a description — the flow's record should say what needs attention.
Don't
Reach for Tabs because the layout looks similar — tab semantics promise free navigation a wizard doesn't offer.

On this page