Stepper

Free · MIT

Horizontal or vertical stepper where the connecting line fills between steps, numbers morph into drawn checkmarks, a current-step ring glides to the active step, errors shake, and completed steps are clickable.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/stepper.json

The file lands in components/goodui/stepper.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/stepper works too.

Usage

import { Stepper } from "@/components/goodui/stepper"
import { Button } from "@/components/ui/button"

const steps = [
  { title: "Service", description: "Balayage + gloss" },
  { title: "Stylist", description: "Priya" },
  { title: "Time", description: "Thu 2:30 PM" },
  { title: "Confirm" },
]

export function BookingWizard({ step, setStep }: { step: number; setStep: (n: number) => void }) {
  return (
    <div className="flex flex-col gap-6">
      <Stepper steps={steps} value={step} onValueChange={setStep} aria-label="Booking progress" />
      <div className="flex justify-between">
        <Button variant="outline" disabled={step === 0} onClick={() => setStep(step - 1)}>
          Back
        </Button>
        <Button onClick={() => setStep(Math.min(steps.length, step + 1))}>
          {step === steps.length - 1 ? "Book" : "Continue"}
        </Button>
      </div>
    </div>
  )
}

When to use

Use it for
  • Multi-step forms and checkouts: Service → Staff → Time → Confirm.
  • Onboarding checklists in a vertical layout, with each step's form or action opening under it.
  • Showing where the user is in a process and letting them jump back to finished steps.
Reach for something else
  • Background work the user only watches: use multi-step-loader.
  • Switching between independent views: use tabs-indicator.
  • Plain completion percentage without named steps: use progress (variant="steps").
If you needUse
Wizard header above a formorientation="horizontal" (default)
Checklist with inline actionsorientation="vertical" with content on steps
Allow going back onlyclickable="completed" (default)
Free navigation between stepsclickable="all"
Read-only progress displayclickable="none"
Flag a failed steperror: true on that step

Examples

Vertical setup checklist with inline actions

<Stepper
  orientation="vertical"
  value={step}
  onValueChange={setStep}
  aria-label="Setup checklist"
  steps={[
    { title: "Connect your calendar", description: "Google Calendar", content: <Button size="sm" onClick={next}>Connect</Button> },
    { title: "Add your services", description: "3 services, 30–120 min", content: <Button size="sm" onClick={next}>Save services</Button> },
    { title: "Share your link", description: "book.studio.co/lena", content: <Button size="sm" onClick={next}>Copy link</Button> },
  ]}
/>

Payment step failed

<Stepper
  value={2}
  clickable="none"
  aria-label="Checkout"
  steps={[{ title: "Cart" }, { title: "Details" }, { title: "Payment", error: paymentFailed }, { title: "Done" }]}
/>

API reference

PropTypeDefault
steps*
The steps, in order.
StepperStep[]—
value
Index of the current step. Use `steps.length` to show every step as complete.
number—
defaultValue
Starting step when uncontrolled.
number0
onValueChange
Called with the step index when a clickable step is clicked.
(index: number) => void—
orientation
horizontal: steps in a row with titles under the circles. vertical: a column that can open content under the current step.
"horizontal" | "vertical""horizontal"
clickable
Which steps can be clicked to jump to them.
"completed" | "all" | "none""completed"
labels
Text for built-in controls and screen readers; pass translations here.
Partial<StepperLabels>—

Also accepts every <ol> attribute.

Accessibility

TabMoves between clickable steps (they are native buttons); other steps are not focusable.
Enter / SpaceJumps to the focused clickable step.
  • Renders an <ol> of <li> items; the current one has aria-current="step". Pass aria-label on the stepper to name it.
  • Each title is followed by an sr-only status ("completed", "current step", "not started", "needs attention"), translatable through labels.
  • Clickable steps are real <button>s with a focus-visible ring; non-clickable steps are plain elements, so they are not in the tab order.
  • The error state is shown by a red circle, a "!" mark and red title text, plus the sr-only status.

Motion

  • The line between two steps fills when the earlier one completes (0.45 s, ease [0.32, 0.72, 0, 1]).
  • A completed step's number swaps for a check that draws in 300 ms while the circle pops to 1.14x and back in 0.4 s; an error step shakes sideways for 0.4 s.
  • A soft ring around the current step glides to the next one through a shared layout animation (spring 300 / 30). Circle colors cross-fade over 300 ms.
  • Vertical content under the current step opens to its height (0.4 s, no bounce).
  • Reduced motion: no pop or shake, the check and lines appear at once, and the ring and content jump.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • value is the 0-based index of the current step; value={steps.length} shows every step complete.
  • The stepper does not render Next/Back buttons or validate anything; keep the index in your state and change it from your own buttons.
  • onValueChange only fires when a clickable step is clicked, not when you change value yourself.
  • content only renders in the vertical orientation, under the current step.
  • error overrides the status of that step wherever it is; clear it once the problem is fixed.

FAQ

Can users skip ahead?

Only with clickable="all". The default "completed" lets them go back to finished steps but not forward.

Does it work on mobile?

The horizontal layout keeps every step in one row and hides descriptions below the sm breakpoint; for many steps or long titles use orientation="vertical".

Related