Navigation
Stepper
A horizontal wizard progress indicator — completed steps are clickable to go back, the current and future steps never are.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/stepper.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Stepper" component (deps: lucide-react
for the check icon).
Contract
- export const Stepper = React.forwardRef<HTMLOListElement, StepperProps>
rendering an <ol>; spread remaining native ol props on the root, merge
className via cn().
- steps: { id: string; label: string; description?: string }[] — description
is optional per-step caption text.
- current: number — 0-based index of the active step; the component holds no
internal state, the consumer drives it.
- onStepClick?: (index: number) => void — when provided, completed steps
(index < current) render as real <button>s that call it; the current step
and any future step always render as a plain <div>, regardless of whether
onStepClick is passed — so users can only ever jump backward, never skip
ahead.
Behavior
- Derive per-step status from index vs current: index < current = "complete",
index === current = "current", otherwise "upcoming".
- The connector line between step i and step i+1 is bg-primary when step i is
complete (index < current), otherwise bg-border — so the filled portion
always tracks how far the user has actually progressed, not just the
current position.
- Clicking a completed step's circle calls onStepClick(index); the button's
accessible name is "Go back to step {n}: {label}" since the circle's
glyph (number or check) is aria-hidden and conveys nothing on its own.
- The active <li> carries aria-current="step"; the circle's inner number/Check
is decorative (aria-hidden) — the visible label text underneath is what a
screen reader actually announces for each step.
- Narrow screens: only the current step's label/description stay visible
below the sm breakpoint (hidden sm:flex on the rest) so a long step list
never overflows or wraps into a jumbled column; the circles and connectors
always stay in a single row regardless of width.
Rendering & styling
- Semantic tokens only. Complete circle: border-primary bg-primary
text-primary-foreground with a lucide Check. Current circle: border-primary
text-primary plus a ring-2 ring-primary/25 ring-offset-2
ring-offset-background halo. Upcoming circle: border-muted
text-muted-foreground. The current step's label is font-medium
text-foreground; every other label and every description stay
text-muted-foreground.
- Layout: <ol className="flex w-full">, each <li> is a flex row of
[circle+label column, connector] where the column is shrink-0 and the
connector is flex-1 (only non-last steps grow to fill the gap); the
connector sits at mt-[18px] so it lines up with the circle's vertical
center (half of the size-9 circle).
- Focus-visible ring on the clickable button; merge the consumer's className
onto the root <ol> via cn().
Customization levers
- Vertical variant: swap the <ol> to flex-col, turn the connector's flex-1
width into a flex-1 height bar between stacked circles, and let labels sit
beside the circle instead of below it — the status/click logic is
orientation-independent.
- Click policy: pass onStepClick to let completed steps jump back (the
default here), omit it entirely for a fully static/read-only progress
display, or loosen the "complete" condition in the clickable check to let
users click any step — including upcoming ones — if the flow doesn't need
to guard against skipping.
- Connector animation: add a transition-[width] (or a scaleX transform)
driven by the same index < current condition to animate the fill sweeping
in as current advances, instead of the default instant color swap.
- Density/size: size-9 circles and the sm label breakpoint are the only two
sizing knobs — swap to size-7/size-11 and adjust mt-[18px] to match the new
half-height, or move the breakpoint (sm → md) to keep labels hidden longer
on medium screens.Concepts
- No-skip-ahead gating — only steps with
index < currentever render as a button; the current step and every step ahead of it always render as an inertdiv, soonStepClickcan only ever move the user backward. - Status derived from two numbers —
indexandcurrentare the only inputs; there's no separate "completed steps" array to keep in sync, so the visual state can never drift from the position the consumer already tracks. - Connector as progress trail — each line segment mirrors whether the step behind it is done, turning the row of circles into one continuous progress trail rather than N independent badges.
- Decorative glyph, spoken label — the number/check inside the circle is
aria-hidden; the accessible name comes from the visible label (or the button's explicitaria-label), so screen readers hear "Account, Profile, Confirm" instead of "1, 2, 3". - Responsive label collapse — hiding non-current labels below
smkeeps the step row itself from ever needing horizontal scroll; only the active step's caption is guaranteed visible at every width.