Stepper
Progress through a sequence of steps, drawn as a read-only indicator showing which steps are complete, which one is current, and which are still to come.
Preview
stepper · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/stepper
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { Stepper } from "@/components/ui/stepper"<Stepper
current={1}
steps={[
{ label: "Your details" },
{ label: "Your preferences" },
{ label: "Review" },
]}
/>When to use it
Use it when
- Showing a reader where they are in a short, fixed sequence, beside the current step's own content.
- Giving a multi step flow a sense of length, so the reader sees how many steps remain.
- Reflecting progress the product already tracks, where each step's state comes from the product.
Do not use it when
- You want several questions on one screen, with a bar of steps standing in for a real form.Use
fieldinstead. - The items are separate views the reader switches between at will, not a sequence taken in order.Use
tabsinstead. - You need one continuous measure of how far through a single task the reader is.Use
progressinstead.
Anatomy
StepperThe root, an ordered list. Draws steps in the order supplied; carries no clinical axis.Controlled bystepsStepper.StepOne step, a list item. State comes from its index against current: complete, current, or upcoming.Controlled bycurrentStepper.MarkerThe circular mark. Solid with a check glyph when complete, a heavier ring when current, faint otherwise.Stepper.LabelThe step's words, with an sr-only state word beside them and an optional muted description beneath.Stepper.ConnectorThe line to the next marker. Stronger below a complete step, decorative, omitted after the last.
- Stepper 1an ordered list; list order is reading order
- Stepper.Step 2..na list item; aria-current="step" on the current one
- Stepper.Marker 1check glyph when complete, else the 1-based number
- Stepper.Label 1carries an sr-only state word
- Stepper.Connector 0..1aria-hidden; omitted after the last
- Stepper.Step 2..na list item; aria-current="step" on the current one
Examples
A setup flow in progress
Five steps, two complete, one current, two to come, driven by current.
stepper-setup-flow · base base · style base-lyraOpen under the product themeSteps with supporting descriptions
Each step's optional description rides beneath its label in muted footnote
ink. Use it to clarify what a step covers, never to give an instruction: this
indicator cannot be operated.
stepper-with-descriptions · base base · style base-lyraOpen under the product themeContent guidelines
Keep labels short, parallel and in sentence case, with no terminal punctuation, named from the reader's side. Order them the way the reader moves, since list order is draw order. Write a description only when the label leaves a real question open.
Do
"Your details" then "Your preferences" then "Review" reads as one set.
Don’t
"Step 1: fill in everything about yourself" then "Prefs" mixes lengths and grammars.
Accessibility
Audited against WCAG 2.2 AA, in a source pass and a rendered pass. The audit is author-run, not independent, and clinical review is still pending.
Fixed. The root list had no accessible name. An optional label now
forwards to the root <ol> as aria-label, set on the default and both
examples; omitted, the attribute is dropped rather than shipping empty.
Confirmed sound.
<ol>root and<li>steps, so a screen reader announces a list of known length, in order.- The current step carries
aria-current="step". - State never rests on colour alone: glyph, fill weight, border weight, label weight, list position, and an sr-only "Completed", "Current step" or "Upcoming" all carry it.
- The check glyph is
aria-hidden; the indicator takes no tab stop, since it has nothing to operate.
Residual.
- The upcoming marker's hairline border against the page is an unmeasured contrast pair. Meaning does not rest on it: number, position, the sr-only word and label weight already carry upcoming state.
- No live region announces a change of
current. The parent flow owns focus and announces each new page, soaria-livewould fire on unrelated renders. - A consumer who omits
labelships an unnamed list. The default and both examples set it, the recommended usage.
| Key | Action | Notes |
|---|---|---|
| Tab | Skips the stepper | Nothing to operate, so no tab stop; focus lands on the next interactive element. |
| Shift+Tab | Skips the stepper | Passed in reverse, moving focus to the previous interactive element. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
API reference
Prop
Type
Generated from StepperProps in registry/bases/base/stepper.tsx.
current is a 0-based index into steps; the product owns position, the
component keeps no state of its own. An out-of-range or non-integer value is
clamped to the nearest real step and raises a development warning.
Related
TabBar
The persistent bar of top-level destinations at the foot of a phone screen, marking one as current with a heavier label and a top indicator rather than colour.
Tabs
A row of tabs that switches between panels sharing one region. The list is one tab stop with a roving focus, and choosing a tab swaps the panel.