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

  1. Completed: AccountEmail + password
  2. 2ProfileName + avatar
  3. 3BillingPlan + card
  4. 4ReviewConfirm + submit

Error state — status="error" on a step

  1. Completed: Upload
  2. Error: Validate3 rows failed schema
  3. 3Publish

Vertical, clickable — jump between steps

Installation

pnpm
pnpm dlx @hex-core/cli add stepper

With error state

Mark a failed step explicitly with status='error'

tsx
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

tsx
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 direction
horizontal
  1. Completed: One
  2. 2Two
  3. 3Three
vertical
  1. Completed: One
  2. 2Two
  3. 3Three
ValueDescription
horizontaldefault
Steps laid out left-to-right
vertical
Steps stacked top-to-bottom
sizeIndicator size
sm
  1. Completed: One
  2. 2Two
  3. 3Three
md
  1. Completed: One
  2. 2Two
  3. 3Three
ValueDescription
sm
Compact indicator (1.75rem)
mddefault
Default indicator (matches control-height-sm)

API Reference

PropTypeDefaultDescription
stepsrequired
objectOrdered list of { id, label, description?, disabled?, status? }. `status` overrides the index-derived value.
currentrequired
numberIndex of the current step (controlled).
orientation
stringhorizontalLayout direction: 'horizontal' | 'vertical'
size
stringmdIndicator size: 'sm' | 'md'
onStepClick
functionWhen provided, each step renders as a clickable <button>; otherwise steps are non-interactive <span>s. Signature: (index: number) => void
aria-labelrequired
stringRequired 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