Stepper
Guide a user through an ordered, often-gated process — showing where they are, what's done, and what's left.
@garn/ui/stepperDefault
A horizontal checkout — labels beside the indicators, filled connectors behind completed steps.
Layout
Horizontal
A horizontal checkout — labels beside the indicators, filled connectors behind completed steps.
Label Below
labelPlacement="bottom" — labels under each indicator.
Vertical
A vertical flow-rail timeline.
Sub Steps
subStep — finer dot markers nested under a milestone (best in vertical).
Indicator
Dots
variant="dot" — minimal dot indicators.
Custom Icons
A per-step `icon` that persists across states.
Outline
look="outline" — the Carbon language (dashed upcoming, dark-neutral complete, ! error, per-step top tails).
State
States
Every step state — complete · current · error · optional · disabled.
Loading
`loading` — a spinner in the indicator while a step processes.
Sizes
sm · md · lg.
Behavior
Clickable
linear={false} — click any step to jump (controlled value + onValueChange).
Wizard
The full wizard — StepContent panels, StepperCompleted, Prev/Next, and an async onBeforeStepChange gate.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add stepper- Registry
stepper- Source
- stepper.tsx
- Deps
@radix-ui/react-slotclass-variance-authoritylucide-react- Registry deps
utilsuse-controllable-statespinner
Import
import { Stepper } from "@garn/ui/stepper";Props
Generated from the component's TypeScript types.
labelPlacement- Description
- Horizontal only: label beside the indicator (`end`) or under it (`bottom`).
- Type
"bottom" | "end"- Default
end
look- Description
- Visual language for numbered indicators: solid `filled` circles (default) or the Carbon-style `outline` look (dashed upcoming, dark-neutral complete, `!` on error, per-step top tails in horizontal). Dots are look-agnostic.
- Type
"filled" | "outline"- Default
filled
orientation- Description
- No description.
- Type
"horizontal" | "vertical"- Default
horizontal
size- Description
- No description.
- Type
"lg" | "md" | "sm"- Default
md
variant- Description
- Indicator shape: numbered circle (default) or a dot.
- Type
"dot" | "numbered"- Default
numbered
defaultValue- Description
- Uncontrolled initial active step index.
- Type
number
label- Description
- Accessible name for the step list (the `nav` landmark).
- Type
string
linear- Description
- Gate forward navigation (default true); non-linear lets any step be clicked.
- Type
boolean
onBeforeStepChange- Description
- Async forward-gate. Return `false` (or a Promise of it) to block the move; the target indicator shows a spinner while pending. Back is never gated.
- Type
(details: BeforeStepChangeDetails) => boolean | Promise<boolean>
onValueChange- Description
- No description.
- Type
(index: number) => void
value- Description
- Controlled active step index (0-based).
- Type
number
Plus 278 inherited native <button> attributes.
Step
completedIcon- Description
- Custom completed indicator (overrides the check).
- Type
React.ReactNode
description- Description
- Terse mode: secondary text under the title.
- Type
React.ReactNode
disabled- Description
- No description.
- Type
boolean
error- Description
- Mark the step as failed.
- Type
boolean
icon- Description
- Custom indicator content (overrides the number/dot).
- Type
React.ReactNode
loading- Description
- Show a spinner in the indicator (e.g. this step is processing).
- Type
boolean
optional- Description
- Mark the step optional (renders an "Optional" caption).
- Type
boolean
subStep- Description
- Render as a smaller sub-step dot in the rail (a finer-grained marker that still counts in the sequence). Best in vertical orientation.
- Type
boolean
title- Description
- Terse mode: the step's label.
- Type
React.ReactNode
value- Description
- Stable id, used to match a `StepContent` panel by value (optional).
- Type
string
Plus 278 inherited native HTML attributes.
StepContent
value- Description
- Match this panel to a step by its `value` (otherwise matched by position).
- Type
string
Plus 280 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="stepper"]| Property | Token | Tier |
|---|---|---|
sectionGap | --garn-gap-section | semantic · air |
motion | --garn-motion-base | semantic |
ring | --garn-ringbrandable | semantic |
Private instance vars (never externalize): --stepper-progress
Overview
Stepper guides a user through an ordered, often-gated process — a checkout, an onboarding wizard, a multi-section form — showing where they are, what's done, and what's left. StepperList holds each Step (with its StepIndicator, StepTitle, and StepSeparator), reflecting complete / current / upcoming state. Use it for sequential, dependent steps; for switching between peer views use Tabs, and to show where a page sits in a site hierarchy use Breadcrumb.
Guidelines
Use a stepper only for a genuine sequence. It implies order and progress — right for a flow with dependent stages, wrong for independent sections (that's Tabs). Keep the number of steps small and group sub-tasks rather than exposing a dozen.
Always communicate position and status. Make the current step, completed steps, and what remains obvious; name each step by its outcome ("Shipping", "Payment") so users can anticipate the path. Show validation per step before advancing.
Be clear about gating and back-navigation. If steps are gated, explain why a later step is locked; let users go back to completed steps without losing data. Keep it keyboard-navigable and announce step changes for assistive tech.
Best practices
- Give the step list an accessible name (the Stepper
label, or aria-label on StepperList). - Control the active step via
value+onValueChange, or leave it uncontrolled withdefaultValue— the value is the 0-based INDEX. - Gate forward moves with
onBeforeStepChange(return false / a Promise of it); Back is always free. - Add StepContent panels (matched by position) to own the content, or omit them for a route-based wizard.
- Use
linear(default) for a gated flow; passlinear={false}to let any step be clicked.
- Don't use a stepper for unordered peer views — that's tabs.
- Don't pass a step id to
value; it's the 0-based step index. - Don't combine
variant="dot"withlookexpecting a change — dots are look-agnostic. - Don't strip the focus ring or the aria-current wiring — they are the keyboard + screen-reader contract.
Content guidelines
- Keep step titles short and parallel (one or two words) so the rail fits on one line.
- Use
descriptionfor a brief subtitle; mark genuinely skippable stepsoptional.
Accessibility
- Role
navigation- Focus
- 2px ring in --garn-ring + offset on interactive step triggers, Prev/Next, and the active panel.
Enforceable contracts
Give the step list an accessible name via the Stepper `label` prop, or `aria-label` on StepperList.
when the StepperList has no `aria-label` / `aria-labelledby` and the Stepper has no `label`
Keyboard
| Tab | Move between interactive controls — clickable steps and the Prev/Next triggers. |
| EnterSpace | Activate the focused step (non-linear) or Prev/Next trigger. |
- • The root is a <nav> landmark > <ol>; each Step is an <li>; the active step's <li> (and its trigger, if interactive) carries aria-current="step".
- • Indicators are aria-hidden; status reaches assistive tech via a single role=status, aria-live=polite region announcing "Step N of M: <title>".
- • StepContent is role=region, aria-labelledby its StepTitle id, hidden when inactive; an interactive step aria-controls its panel ONLY when one exists.
- • The tablist model is deliberately avoided — it implies free navigation that conflicts with linear gating, and the roles-without-keyboard-contract trap.
- • Give the step list an accessible name via the Stepper `label` prop or aria-label on StepperList.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.