Dev view

Stepper

A sequential multi-step indicator that visualises progress through an ordered process — checkout flows, signup wizards, multi-step forms, onboarding sequences. Distinct from Tabs (which switches between non-sequential parallel views) by enforcing order: steps complete in sequence, prior steps may be revisited but later steps are gated until prior steps validate. Distinct from Accordion (independent collapsible regions) by the strict sequence and the visible progress-through-N-steps state.

When to use

Use

For sequential multi-step processes where the order matters and progress through-the-flow is meaningful — checkout flows, signup wizards, multi-step forms, onboarding sequences, document signing flows. The user must (or typically does) complete the steps in sequence; their progress is visible at all times.

Avoid

For parallel views of the same content — that is `Tabs`. For independent collapsible regions — that is `Accordion`. For navigation between top-level pages — that is `SidebarNav`. For sequences exceeding ~7 steps — redesign as a wizard with phase grouping or as a multi-page flow.

Versus related

  • tabs

    `Tabs` is parallel — peer panels switchable in any order. `Stepper` is sequential — ordered steps that complete in sequence. The semantic distinction drives the role (`role="tablist"` with `aria-selected` vs `<ol>` with `aria-current="step"`). Use Tabs for non-sequential views; Stepper for ordered progress.

  • accordion

    `Accordion` is a list of independent collapsible regions; `Stepper` is a sequential progression with gating between steps. Accordion items are peers with no ordering; Stepper steps are ordered and gated. They look superficially similar (vertical list of expandable items) but the semantic distinction is clear in canonical usage.

  • sidebar-nav

    `SidebarNav` navigates between independent pages with their own URLs; `Stepper` shows progress within a single multi-step flow. SidebarNav is persistent global navigation; Stepper is contextual to the current flow.

Highlight
Fig 1.1 · Stepper · Dev view
Dev

Code anatomy

Slot Code slot Semantic
root root progress-indicator
step step listitem
step-indicator step-indicator presentational
step-label step-label text
step-description step-description text
connector connector presentational
Both

Variants, properties, states

Variants

Structurally different versions of the component.

standarddotvertical

Properties

The same component, parameterised.

PropertyType
nonLinearboolean
hasDescriptionsboolean
sizesm | md | lg

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibledisabled
data
pendingactivecompletederror
Both

State transitions

FromToTrigger
pendingactiveUser progresses to this step — by completing the prior step (linear mode) or by clicking the step directly (`nonLinear: true`). The previously- active step transitions out of active; this step becomes active and `aria-current="step"`.
activecompletedUser completes the current step (validation passes, "Next" activated). The step transitions out of active; the next step becomes active. The completed step's indicator changes to a checkmark.
activeerrorUser attempts to advance from this step but validation fails. Step gains error state; the step's indicator changes to error glyph; an inline error message describes the failure. Stays as the active step — the user must resolve the error before advancing.
erroractiveUser resolves the validation error. State returns to active; error indicator clears.
completedactiveUser navigates back to a previous step (per `nonLinear: true` or the canonical "user may revisit prior steps" pattern). Previously-active step transitions to its appropriate state (typically pending if not yet completed, or completed if revisiting after completion).
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-stepper>` host with `<ui-step>` children; `<ol>` element internally; named slots for label and description per step attributes (`variant="vertical"`, `non-linear`, `size="md"`); per-step `state` attribute (pending / active / completed / error); `data-current` for active step
React Compound components (`<Stepper>` plus `<Stepper.Step>` plus `<Stepper.StepLabel>` plus `<Stepper.StepDescription>`); React Aria does not ship a Stepper primitive but `useListBox` plus custom step semantics works; Material UI ships `<Stepper>` with `<Step>` and `<StepLabel>` matching the canonical structure props with class-variance-authority for variant / size; controlled or uncontrolled `activeStep` index; per-step `completed`, `error` state props
Angular (signals) Angular Material `MatStepper` plus `MatStep`; or Angular CDK Stepper for headless composition; signal-based active-step state input<'standard' | 'dot' | 'vertical'>(); `[nonLinear]`, `[size]` host bindings; per-step `[state]` input
Vue Vuetify `<v-stepper>` plus `<v-stepper-item>`; or custom composition with `<ol>` plus per-step components defineProps with literal-union types; `:variant`, `:non-linear` props
Both

Events

  1. stepActivate
    Payload
    `{ stepId: string, stepIndex: number }`. Fires when the user navigates to a step — by clicking (in `nonLinear: true` mode), by completing the prior step in linear mode, or by programmatic `setActiveStep`. The previously-active step transitions out of active.
    Web Components
    `stepActivate` CustomEvent on the host with `event.detail = { stepId, stepIndex }`.
    React
    `onStepChange(activeStep: number)` controlled- pattern callback (Material UI Stepper pattern).
    Angular Signals
    `output<{ stepId: string, stepIndex: number }>('stepActivate')`.
    Vue
    `@update:activeStep` for `v-model:active-step`.
  2. stepComplete
    Payload
    `{ stepId: string, stepIndex: number }`. Fires when a step transitions to completed state — typically on "Next" activation after validation passes. Distinct from `stepActivate` (which fires on navigation, regardless of completion).
    Web Components
    `stepComplete` CustomEvent with `event.detail = { stepId, stepIndex }`.
    React
    `onStepComplete(stepIndex: number)` callback.
    Angular Signals
    `output<{ stepId: string, stepIndex: number }>('stepComplete')`.
    Vue
    `@step-complete` event.
  3. stepError
    Payload
    `{ stepId: string, stepIndex: number, message: string }`. Fires when a step transitions to error state — typically when "Next" is activated but validation fails. The message is the user-facing failure description.
    Web Components
    `stepError` CustomEvent with `event.detail = { stepId, stepIndex, message }`.
    React
    `onStepError(stepIndex: number, message: string)` callback.
    Angular Signals
    `output<{ stepId: string, stepIndex: number, message: string }>('stepError')`.
    Vue
    `@step-error` event.
Dev

Performance thresholds

  • maxStepsBeforeRedesignstep-count7steps

    Above ~7 steps, the cognitive load of remembering progress through the sequence overwhelms the stepper pattern. Long sequences should be redesigned as a wizard with section grouping (group steps into logical phases), or split into multi-page flows where each page has its own short stepper. The threshold matches the "7 plus or minus 2" working- memory canonical guideline (Miller's law).

Both

Accessibility

Slot Accessibility hint
root Apply `role="list"` (or `<ol>` for the canonical ordered-list semantic) plus `aria-label="Progress"` or "Steps". Each step is a `<li>` with role inferred or explicit. The current step also carries `aria-current="step"` to announce position to SR users.
step Each step is a `<li>` carrying state via `aria-current="step"` (current), `aria-disabled` (disabled future steps in linear mode), or no attribute (pending / completed). For interactive steppers (where the user may click prior steps to revisit), the step is wrapped in a `<button>` carrying the accessible name from the label.
step-indicator Decorative — the step's accessible state (`aria-current`, `aria-disabled`) plus visually- hidden text ("Step 2 of 5, complete") communicates state. The number / checkmark / error glyph is visual reinforcement only. SR users do not rely on the indicator glyph.
step-label Plain text. The step's accessible name. For interactive steps wrapped in `<button>`, the label is the button's text content. For dot variant (label visually-hidden), the label remains in the DOM via sr-only — never `display: none`.
step-description Plain text. SR users hear it after the label when navigating into the step. For interactive steps, the description is referenced via `aria-describedby` on the step's button to compose into the announcement.
connector Decorative — `aria-hidden="true"`. Sequence is communicated through DOM order plus the steps' position cues (`aria-current`, `aria-disabled`, visually-hidden "step N of M" text); the line is visual reinforcement.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFor interactive steppers, focus enters the current step's button; subsequent Tab moves to the next focusable in the document (or to the "Next" button if present below the stepper). Disabled future steps in linear mode are skipped from tab order.
ArrowLeft / ArrowRight (focus on a step, horizontal orientation)For non-linear steppers, moves focus to the previous / next step via roving tabindex. Activation does NOT auto-occur — Enter / Space activates. Linear steppers do not bind ArrowKeys on steps because future steps are disabled.
ArrowUp / ArrowDown (vertical orientation)Same as ArrowLeft / Right for vertical layouts.
Enter or Space (focus on step)Activates the step — navigates to it (non-linear) or is a no-op for already-active step. For completed steps in linear mode, activation navigates back to revisit (canonical "may revisit prior steps" allowance).
Home / End (focus on step)Moves focus to first / last step in non-linear mode. In linear mode, Home is the first completed or active step; End is the active step (future steps not focusable).

Screen-reader announcements

TriggerExpected
Focus enters current stepSR announces "Step <N> of <M>, <label>, current step, button" (or "link" if stepper-as-nav). The position cue comes from visually-hidden text or `aria-describedby`; the current state from `aria-current="step"`; the label from text content.
Step transitions to completedSR announces the new state via the next announcement on focus change; the visual checkmark is decorative. For consumers using a polite live region to announce step completion, the announcement reads "Step 2 of 5 complete; Step 3 of 5, Payment, now active".
Step transitions to errorSR announces the error state via the polite live region: "Error in Step 2 of 5, Shipping: <error message>". Focus moves to the first invalid field within the step's content. `aria-invalid="true"` on the field.
Stepper completes (final step → completed)SR announces completion via the live region: "Process complete." Focus moves to the next page or confirmation surface; the stepper itself remains as a record of the completed sequence.

axe-core rules to assert

  • aria-allowed-role
  • aria-required-attr
  • aria-current
  • aria-valid-attr-value
  • color-contrast
  • listitem
  • role-img-alt
Dev

Common mistakes

#stepper-no-aria-current

Active step missing `aria-current`

Problem

Visual active step is shown via background colour or indicator emphasis; `aria-current="step"` is missing. SR users hear no signal of which step is active; they cannot orient to the position in the sequence.

Fix

The active step carries `aria-current="step"` (the canonical APG-recommended value for sequence steps). SR announces "current step" before the step's label. Update on every step transition.

#stepper-skip-validation

Linear stepper allows skipping required steps

Problem

User clicks step 4 in linear mode while step 2 is incomplete; the click navigates them there regardless. Required validation per step is bypassed; the underlying flow breaks downstream.

Fix

In linear mode (`nonLinear: false`), future steps are `aria-disabled` until prior steps complete. Click is a no-op (or shows a "complete prior steps first" message). The "Next" button is the canonical forward-navigation; clicking step indicators directly is allowed for prior steps but not future steps.

#stepper-step-not-button

Interactive step not a button

Problem

Implementation makes step indicators clickable via onClick on a `<div>`. SR users do not get button semantics; keyboard users cannot activate via Enter / Space; focus management is bespoke.

Fix

For interactive steps (any step the user may navigate to via click), wrap in `<button>` (or `<a>` for stepper-as-navigation patterns where each step has its own URL). Standard button semantics apply: focus, Enter / Space activation, accessible name from text content.

#stepper-no-keyboard-nav

Steps reachable only via mouse

Problem

Implementation lacks keyboard support — Tab cycles through every step but ArrowKeys do not navigate between steps; or the steps are not focusable at all.

Fix

Steps are reachable via Tab (when interactive). For non-linear steppers, ArrowLeft / ArrowRight (or ArrowUp / ArrowDown for vertical orientation) move focus between steps via roving tabindex; Enter / Space activates. Linear steppers may use Tab-only navigation since future steps are disabled.

#stepper-progress-cues-decorative-only

Progress communicated only by visual indicators

Problem

Visual indicators (numbered circles, checkmarks, coloured states) communicate progress. SR users hear no progress percentage or "step N of M" composition. The visual story is rich; the accessible story is empty.

Fix

Compose visually-hidden "Step N of M" text into each step's accessible name. The stepper's root may additionally carry `aria-valuenow` / `aria-valuemin` / `aria-valuemax` if treating the stepper as a progress indicator (rare; usually the per-step announcement is sufficient).

Figma↔Code mismatches
  1. 01
    Figma

    Stepper drawn as horizontal Tabs

    Code

    A list of steps with `aria-current="step"` plus order semantics

    Consequence

    Designers may use Tabs visually for sequential flow. Implementations following the design ship `role="tablist"` with `aria-selected` instead of `aria-current="step"`. SR users hear "tab" with no sense of sequence; the "X of N steps" announcement is missing; users navigate between tabs and bypass step ordering.

    Correct

    Distinguish at the canonical level: Stepper is sequential and ordered; Tabs is parallel and switchable. The semantic distinction drives the role (`<ol>` / `role="list"` vs `role="tablist"`) and the state (`aria-current="step"` vs `aria-selected`). Use Tabs for parallel views; Stepper for sequential progression.

  2. 02
    Figma

    Step indicator drawn but no accessibility for "step N of M"

    Code

    Each step carries visually-hidden "Step 2 of 5" text in addition to the indicator number

    Consequence

    Designers compose mocks with the indicator showing "2" inside a circle. Implementations following the design ship the visual indicator only; SR users hear the indicator number announced as a digit but without context — they do not know how many steps remain or which step is current.

    Correct

    Pair the visual indicator with visually-hidden text composing the position into the step's accessible name: "Step 2 of 5, Shipping, current". The visually-hidden text composes via `aria-describedby` or directly inside the step container. The indicator number itself can be `aria-hidden`.

  3. 03
    Figma

    Sequential and non-linear stepper drawn as the same component

    Code

    A `nonLinear: boolean` property that gates whether the user may click prior or future steps

    Consequence

    Designers do not distinguish linear (one-direction- only) from non-linear (jump-anywhere). Implementations following the design may ship the wrong mode; users expecting to jump to a future step in linear mode get blocked silently; users expecting linear sequence in non-linear mode skip required steps and the underlying flow breaks downstream.

    Correct

    Document `nonLinear` as a first-class property. Linear (default false): only completed and current steps are reachable; future steps are `aria-disabled`. Non-linear (true): all steps reachable; user navigates freely. Visual treatment communicates the mode (disabled-future- steps in linear mode are clearly muted and unclickable).

  4. 04
    Figma

    Connectors drawn as a single uniform line

    Code

    Connectors with style per adjacent-step-state pair (solid for completed-to-completed, muted for pending)

    Consequence

    Designers may use one connector style throughout for visual simplicity. Implementations following the design ship a single uniform line; users cannot distinguish completed sequence from pending sequence visually. The progress-through-N-steps story is lost.

    Correct

    Connector style varies based on the states of its flanking steps. Solid line for completed-to-completed (or completed-to-active); muted dashed or thinner line for pending-to-pending. Visually communicates progress-flow direction. Decorative — the canonical semantic still lives in `aria-current` and step states.