Stepper
The step indicator strip for a multi-step flow — Stepper holds the
state and navigation, StepperStep renders each step. Status
(upcoming/current/complete, or error if you set it) is derived
from position, so you only ever manage one number: the current step.
Navigation defaults to strictly linear — a step is only ever clickable if
it’s complete or current, so Stepper never lets someone skip ahead to a
step they haven’t reached. Pass canNavigateTo to override this for a
non-linear flow (e.g. every step the user has already validated, not
just the ones before the current position).
Step position can optionally resume across sessions — pass persistKey
to opt in. It’s off by default, same as every other persisted piece of
state in this package.
Playground
- 3Invite your team
- 4Confirm
Install
npm install @runmarklabs/react/* your app's CSS entry point */@import "@runmarklabs/react/stepper/style.css";import { Stepper, StepperStep } from "@runmarklabs/react/stepper";
<Stepper aria-label="Onboarding progress" step={step} onStepChange={setStep}> <StepperStep label="Account" description="Create your login" /> <StepperStep label="Workspace" /> <StepperStep label="Confirm" /></Stepper>;Props
Stepper
| Prop | Type | Default | Description |
|---|---|---|---|
allowNavigation | boolean | true | Whether activating a step's indicator navigates to it at all. Set to `false` for a purely informational, unclickable progress indicator. |
aria-label | string | — | Accessible label for the step list, e.g. "Onboarding progress". |
canNavigateTo | ((index: number, status: StepStatus) => boolean) | (_index: number, status: StepStatus) =>
status === "complete" || status === "current" | Per-step reachability predicate, called with each step's index and its computed status. The default only allows navigating to a step that's already `"complete"` or is the `"current"` one — a strictly linear flow. Override this for non-linear navigation (e.g. every step the user has validated so far, not just ones before the current position). Has no effect when `allowNavigation` is `false`, and a step's own `disabled` still wins over this returning `true`. |
children | ReactNode | — | The `StepperStep` elements, in order. Optional — an empty Stepper renders its container with no steps rather than requiring a caller to always have at least one. |
defaultStep | number | 0 | Initial step index in uncontrolled mode. |
onStepChange | ((index: number) => void) | — | Called after the current step changes, controlled or not. |
orientation | "horizontal" | "vertical" | horizontal | |
persistenceAdapter | PersistenceAdapter | a fresh `createLocalStoragePersistenceAdapter()`, only
constructed when `persistKey` is set. | |
persistKey | string | — | Persists the current step index so the flow resumes where the user left off on their next visit. Opt-in — omit to keep step position in memory only, the default for every other controllable/uncontrolled component in this package. |
step | number | — | Controlled current step index (0-based). Omit to let the Stepper manage its own state. |
StepperStep
label (required), description, error, disabled — plus the usual
<li> attributes. See packages/react/src/Stepper/Stepper.tsx for the
full signature; StepperStep’s props aren’t in the table above because it
also receives internal props from its parent Stepper that aren’t part of
what you write.
Accessibility notes
aria-current="step"on the current step, not a color alone — status is also conveyed by the indicator’s icon (a checkmark for complete) and position, so it doesn’t depend on color perception.- Keyboard navigation is roving-tabindex (
@runmarklabs/core’sRovingFocusGroup, the same primitive behind Radix’s own Tabs) — Tab reaches the step strip once, then arrow keys move between reachable steps, Home/End jump to the first/last, and Enter or Space activates the focused one. This follows the WAI-ARIA tabs pattern; the one deliberate deviation is that an unreachable step is never part of the roving-tabindex sequence at all (see the next point), where the tabs pattern would normally still let you arrow onto a disabled tab. - A step that isn’t reachable yet isn’t a disabled button — it’s not a button at all, so it’s never announced as an unavailable control a screen reader user might wonder how to unlock.
- A visually hidden live region announces every step change, including on mount (so resuming from a persisted step announces where you resumed to) — “Step 2 of 4, Workspace,” not just the bare label.
- Zero axe violations — asserted in
packages/react/src/Stepper/Stepper.test.tsx.