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
pnpm dlx @hex-core/cli add stepper

Usage

tsx
import { Stepper } from "@/components/ui/stepper"

Form wizard

Three-step horizontal stepper with the second step active

tsx
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'

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" },
      ]}
    />
  );
}

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