---
title: "Stepper"
description: "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."
url: "https://opsinjs.pensievelabs.org/components/stepper"
source: "https://opsinjs.pensievelabs.org/components/stepper.md"
section: "Components"
status: "shipped"
kind: "component"
category: "navigation"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["wizard", "step indicator", "multi step"]
usedIn: ["diabetes-medicines-app"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="stepper"
  status="shipped"
  questions="[
  &#x22;No contrast pair has been measured, neither the marker fills against the page nor the muted description ink against the surfaces this indicator sits on.&#x22;,
  &#x22;Nobody has listened to this in a screen reader, so whether aria-current and the sr-only \&#x22;Current step\&#x22; double-announce the current step is unchecked.&#x22;,
  &#x22;Forced colours and prefers-contrast: more are unhandled, so whether complete, current and upcoming stay apart once the platform substitutes its own fills is untested.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="stepper" />

## Installation [#installation]

<ComponentInstall name="stepper" unbuilt="false" importPath="@/components/ui/stepper" dependencies="[&#x22;lucide-react&#x22;]" />

## Usage [#usage]

```tsx
import { Stepper } from "@/components/ui/stepper"
```

```tsx
<Stepper
  current={1}
  steps={[
    { label: "Your details" },
    { label: "Your preferences" },
    { label: "Review" },
  ]}
/>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Showing a reader where they are in a short, fixed sequence, beside the current step's own content.&#x22;,
  &#x22;Giving a multi step flow a sense of length, so the reader sees how many steps remain.&#x22;,
  &#x22;Reflecting progress the product already tracks, where each step's state comes from the product.&#x22;,
]"
  avoid="[
  { case: &#x22;You want several questions on one screen, with a bar of steps standing in for a real form.&#x22;, instead: &#x22;field&#x22; },
  { case: &#x22;The items are separate views the reader switches between at will, not a sequence taken in order.&#x22;, instead: &#x22;tabs&#x22; },
  { case: &#x22;You need one continuous measure of how far through a single task the reader is.&#x22;, instead: &#x22;progress&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="stepper"
  parts="[
  {
    name: &#x22;Stepper&#x22;,
    describes: &#x22;The root, an ordered list. Draws steps in the order supplied; carries no clinical axis.&#x22;,
    prop: &#x22;steps&#x22;,
  },
  {
    name: &#x22;Stepper.Step&#x22;,
    describes: &#x22;One step, a list item. State comes from its index against current: complete, current, or upcoming.&#x22;,
    prop: &#x22;current&#x22;,
  },
  {
    name: &#x22;Stepper.Marker&#x22;,
    describes: &#x22;The circular mark. Solid with a check glyph when complete, a heavier ring when current, faint otherwise.&#x22;,
  },
  {
    name: &#x22;Stepper.Label&#x22;,
    describes: &#x22;The step's words, with an sr-only state word beside them and an optional muted description beneath.&#x22;,
  },
  {
    name: &#x22;Stepper.Connector&#x22;,
    describes: &#x22;The line to the next marker. Stronger below a complete step, decorative, omitted after the last.&#x22;,
  },
]"
/>

<CompositionTree
  name="stepper"
  tree="[
  {
    part: &#x22;Stepper&#x22;,
    cardinality: &#x22;1&#x22;,
    note: &#x22;an ordered list; list order is reading order&#x22;,
    children: [
      {
        part: &#x22;Stepper.Step&#x22;,
        cardinality: &#x22;2..n&#x22;,
        note: &#x22;a list item; aria-current=\&#x22;step\&#x22; on the current one&#x22;,
        children: [
          { part: &#x22;Stepper.Marker&#x22;, cardinality: &#x22;1&#x22;, note: &#x22;check glyph when complete, else the 1-based number&#x22; },
          { part: &#x22;Stepper.Label&#x22;, cardinality: &#x22;1&#x22;, note: &#x22;carries an sr-only state word&#x22; },
          { part: &#x22;Stepper.Connector&#x22;, cardinality: &#x22;0..1&#x22;, note: &#x22;aria-hidden; omitted after the last&#x22; },
        ],
      },
    ],
  },
]"
/>

## Examples [#examples]

### A setup flow in progress [#a-setup-flow-in-progress]

Five steps, two complete, one current, two to come, driven by `current`.

<ComponentPreview name="stepper-setup-flow" kind="example" align="start" />

### Steps with supporting descriptions [#steps-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.

<ComponentPreview name="stepper-with-descriptions" kind="example" align="start" />

## Content guidelines [#content-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.

<DoDont>
  <DoDont.Do>
    **"Your details" then "Your preferences" then "Review"** reads as one set.
  </DoDont.Do>

  <DoDont.Dont>
    **"Step 1: fill in everything about yourself" then "Prefs"** mixes lengths and grammars.
  </DoDont.Dont>
</DoDont>

## Accessibility [#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, so `aria-live` would fire on unrelated renders.
* A consumer who omits `label` ships an unnamed list. The default and both
  examples set it, the recommended usage.

<KeyboardTable
  name="stepper"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Skips the stepper&#x22;,
    notes: &#x22;Nothing to operate, so no tab stop; focus lands on the next interactive element.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Skips the stepper&#x22;,
    notes: &#x22;Passed in reverse, moving focus to the previous interactive element.&#x22;,
  },
]"
/>

<ContrastReport component="stepper" />

## API reference [#api-reference]

<PropsTable name="StepperProps" />

`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 [#related]

* [Progress](./progress.mdx) is one continuous measure of a single task.
* [Tabs](./tabs.mdx) is for views a reader picks in any order, with no before and no after.
* [Field](./field.mdx) is where a question lives; a stepper reports position and never collects an answer.
