Skip to main content

Stepper

A stepper shows where the user is in a sequence: completed steps get a check on a --primary circle, the current one a ringed outline, the rest a muted number. It is a <nav aria-label="Progress"> around an ordered list; the current item carries aria-current="step".

import Stepper from '@zuilib/primitives/stepper'

Horizontal

Each step takes an equal share of the width. description is optional secondary text under the label.

Loading example

Vertical

orientation="vertical" stacks the steps with the connector running down the start edge; labels sit beside the circles.

Loading example

Clickable steps

Without onValueChange every step is static text and nothing is in the tab order. With it, completed steps become <button>s so the user can go back; nonLinear also makes the current and upcoming steps clickable.

Loading example
Loading example

Props

StepperProps extends the <nav> attributes with:

PropTypeDefaultDescription
stepsrequiredStepperStep[] — Array<{ id: string; label: string; description?: string }>id is the React key
valuenumberControlled zero-based index of the current step; earlier steps are complete, later ones upcoming
defaultValuenumber0Uncontrolled initial step
onValueChange(value: number) => voidMakes completed steps buttons. Without it every step is static text
nonLinearbooleanfalseWith onValueChange, the current and upcoming steps are clickable too
orientation'horizontal' | 'vertical''horizontal'Exposed as data-orientation
aria-labelstring'Progress'Names the nav landmark
classNamestringOn the nav root

Slots

SlotElementNotes
[data-slot="stepper"]navRoot. Also data-orientation
[data-slot="stepper-list"]ol
[data-slot="stepper-step"]lidata-state="complete" | "current" | "upcoming"; aria-current="step" on the current one
[data-slot="stepper-connector"]divLine to the next step; absent on the last. --primary after a complete step, --border otherwise. aria-hidden
[data-slot="stepper-body"]button / divCircle + text. Also data-state and data-interactive when clickable
[data-slot="stepper-circle"]spanThe numbered / checked circle, a --control-height-md square
[data-slot="stepper-check"]svgThe check glyph on a complete step
[data-slot="stepper-text"]spanWraps label and description
[data-slot="stepper-label"]span
[data-slot="stepper-description"]spanPresent when the step has a description
/* Consumer CSS: square step markers */
[data-slot="stepper-circle"] {
border-radius: var(--radius-sm);
}

Tokens

TokenUsed for
--control-height-mdCircle size and the connector geometry derived from it, so the two stay aligned under any density
--primary, --primary-foregroundComplete circle and connector; current circle border, text and ring
--muted, --muted-foreground, --borderUpcoming circle and connector
--foregroundLabels
--ringKeyboard focus ring on a clickable step
--duration-fast, --duration-normalConnector colour and circle transitions

Accessibility

  • The root is a <nav> landmark named by aria-label ("Progress" by default); pass your own when a page has more than one.
  • Steps are an <ol>, so screen readers announce "1 of 4" and the order. The current li has aria-current="step".
  • Static steps render as divs: nothing to tab onto. Clickable steps are real <button type="button">s, reachable with Tab and activated with Enter or Space, with the shared --ring focus-visible ring.
  • Connectors and the check glyph are aria-hidden; the state is carried by aria-current and the step number.
  • Transitions are disabled under prefers-reduced-motion.
  • Tabs: parallel sections rather than an ordered sequence.
  • Progress: a single bar when the steps have no names.
  • Button: Back / Next controls beside the stepper.