Stepper
Linear progress indicator for multi-step flows (form wizards, onboarding, checkout). Pure semantic <ol>/<li> with aria-current='step' on the active step and a per-step error status override.
Installation
pnpm dlx @hex-core/cli add stepperUsage
import { Stepper } from "@/components/ui/stepper"Form wizard
Three-step horizontal stepper with the second step active
import { Stepper } from "@/components/ui/stepper";
export function Example() {
return (
<Stepper
aria-label="Onboarding"
current={1}
steps={[
{ id: "account", label: "Account", description: "Email + password" },
{ id: "profile", label: "Profile", description: "Name + photo" },
{ id: "confirm", label: "Confirm" },
]}
/>
);
}With error state
Mark a failed step explicitly with status='error'
import { Stepper } from "@/components/ui/stepper";
export function Example() {
return (
<Stepper
aria-label="Checkout"
current={2}
steps={[
{ id: "cart", label: "Cart" },
{ id: "shipping", label: "Shipping", status: "error" },
{ id: "payment", label: "Payment" },
]}
/>
);
}Vertical, clickable
Vertical orientation with onStepClick to jump back
import { Stepper } from "@/components/ui/stepper";
export function Example() {
return (
<Stepper
aria-label="Settings"
orientation="vertical"
current={1}
onStepClick={(i) => console.log(i)}
steps={[
{ id: "profile", label: "Profile" },
{ id: "security", label: "Security" },
{ id: "billing", label: "Billing" },
]}
/>
);
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
stepsrequired | object | — | Ordered list of { id, label, description?, disabled?, status? }. `status` overrides the index-derived value. |
currentrequired | number | — | Index of the current step (controlled). |
orientation | string | horizontal | Layout direction: 'horizontal' | 'vertical' |
size | string | md | Indicator size: 'sm' | 'md' |
onStepClick | function | — | When provided, each step renders as a clickable <button>; otherwise steps are non-interactive <span>s. Signature: (index: number) => void |
aria-labelrequired | string | — | Required accessible name for the ordered list (e.g. 'Onboarding steps', 'Checkout progress') |
AI Guidance
When to use
Use to communicate progress through a multi-step flow with a known fixed sequence: form wizards, onboarding, checkout, ticket triage. Mark per-step error status when validation fails.
When not to use
Don't use for free navigation across unrelated sections (use Tabs). Don't use for indeterminate progress (use Progress with no value). Don't use for >7 steps — collapse into a multi-screen wizard with a sub-stepper instead.
Common mistakes
- Forgetting aria-label — the <ol> needs an accessible name to be understood as a step list
- Setting current to an out-of-range index — derives all steps as 'upcoming'
- Mixing index-derived status with manual status overrides without intent — once you set status on one step, set it on all of them or know the precedence rules
- Making the stepper interactive (onStepClick) but allowing forward jumps before validation — gate jumps in your handler
- Treating it as a tab control — Stepper communicates direction; users can't pick step 5 then go back to 2 to review without your wiring
Accessibility
Renders <ol> with the provided aria-label. The active step's interactive element gets aria-current='step'. Completed steps prefix the label with visually-hidden 'Completed:'; error steps prefix with 'Error:' and set aria-invalid='true' on the indicator. Connector lines are aria-hidden. When onStepClick is omitted, steps are plain <span>s — not fake buttons.
Related components
Token budget: 1400