Stepper
Shows where the user is in a multi-step flow — checkout, onboarding, a wizard — as an ordered list of steps with the current one marked, and lets them go back to steps they have completed.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Stepper } from '@design-schema/react'; Examples
import { Stepper } from '@design-schema/react';
<Stepper
steps={[
{ id: 'shipping', label: 'Shipping address' },
{ id: 'payment', label: 'Payment' },
{ id: 'review', label: 'Review order' },
{ id: 'confirm', label: 'Confirmation' },
]}
current="payment"
/>Props
6 rows| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Accessible name of the navigation landmark. Defaults to `copy.navLabel`, which an empty string also falls back to (`label || copy.navLabel`), so the landmark is never unnamed. |
| steps required | array { id: string; label: string; description?: string; status?: "complete" | "current" | "upcoming" | "error" }[] | — | The steps in order. `status` is derived from `current` when omitted: before it complete, the step it names current, after it upcoming. An explicit `status` sets only the indicator, its colours and the status word; position (not status) decides the selected state, navigability, the connector colour and the compact reveal. |
| current required | string | — | The id of the current step. The step whose id matches is the selected one (`aria-current="step"` / selected state) and the one compact reveals, whatever its `status`. When no id matches, nothing is selected, every step without an explicit status is upcoming, no step is navigable under `completed` (all still are under `all`), the count reads "Step 1 of m", compact reveals the first step's label (so a compact stepper is never label-less), and development builds log a warning — a developer-only English message, not copy, worded identically on every platform: `Stepper: current "<id>" matches no step id; nothing is selected.` The warning needs a non-empty `current`: an empty one (the Lit property's initial `''`) is treated as not yet set and does not warn. |
| orientation | horizontal | vertical | horizontal | Vertical shows descriptions under each label and suits a side column; horizontal does not render descriptions at all (not clipped, and no aria-describedby) and collapses to `compact` below the prose width. |
| navigable | none | completed | all | completed | Which steps can be activated: none (display only), completed steps (the usual — you can go back, not skip ahead), or all (a settings-style flow where order does not matter). "Completed" means visited — any step before the current one by position, including one marked `error`, which is exactly the step a user most needs to return to. The control is the component's own native button, not the Button component, whose single-label API cannot hold an indicator, a label and a description. |
| compact | boolean | false | Show only the current step's label and "Step 2 of 5"; the indicators stay. Horizontal only, set by hand or automatically below the prose width — a vertical stepper has the room, so the prop does nothing there, including the `count`, which a vertical stepper never renders. On web and Lit the labels of the other steps, with their status words, are visually clipped (the visually-hidden technique), not removed, so they stay reachable by a screen reader; on React Native they are not rendered, because each step's accessibilityLabel already carries the label and status. "Step n of m" (`copy.stepOf`) is the `count` part: one muted Text after the list, not in any step and not replacing a description. On web and Lit, where automatic compact is a container query, it is always rendered and `display: none` outside the query (applied through Text's layout-only className); on React Native it is rendered only while compact is in effect. The count follows the list, `stepGap` after it — as the root's own flex/column gap, never a margin, so the gap collapses with the count when it is hidden instead of leaving dead space under every non-compact stepper. |
Events
1 row| Event | Type | Description |
|---|---|---|
| onStepSelect | onStepSelect / step-select | Fired when a navigable step is chosen, with its id. The container changes `current`; the stepper never changes it itself. |
Accessibility
Decisions this component leaves to you
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
Implicit ARIA role: navigation.
Anatomy
What generation guarantees
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).
Selected state — Selection is exposed programmatically (`aria-selected` / `aria-checked`).
This component does not animate, so it has no reduced-motion behaviour to describe.