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.
Form wizard — Back/Next navigation
- Completed: AccountEmail + password
- 2ProfileName + avatar
- 3BillingPlan + card
- 4ReviewConfirm + submit
Error state — status="error" on a step
- Completed: Upload
- Error: Validate3 rows failed schema
- 3Publish
Vertical, clickable — jump between steps
Installation
pnpm dlx @hex-core/cli add stepperWith 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" },
]}
/>
);
}Variant values
orientationLayout directionhorizontal- Completed: One
- 2Two
- 3Three
vertical- Completed: One
- 2Two
- 3Three
| Value | Description |
|---|---|
horizontaldefault | Steps laid out left-to-right |
vertical | Steps stacked top-to-bottom |
sizeIndicator sizesm- Completed: One
- 2Two
- 3Three
md- Completed: One
- 2Two
- 3Three
| Value | Description |
|---|---|
sm | Compact indicator (1.75rem) |
mddefault | Default indicator (matches control-height-sm) |
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
Verified against @hex-core/components@1.12.0