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…

"use client"

import * as React from "react"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"

export interface StepperStep {
  id: string
  label: string
  description?: string
}

export interface StepperProps extends React.HTMLAttributes<HTMLOListElement> {
  steps: StepperStep[]

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/stepper.json

Prompt

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 < current ever render as a button; the current step and every step ahead of it always render as an inert div, so onStepClick can only ever move the user backward.
  • Status derived from two numbersindex and current are 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 explicit aria-label), so screen readers hear "Account, Profile, Confirm" instead of "1, 2, 3".
  • Responsive label collapse — hiding non-current labels below sm keeps the step row itself from ever needing horizontal scroll; only the active step's caption is guaranteed visible at every width.

On This Page