Stepper
ImplementedHorizontal step indicator — 3-6 steps with state (complete/current/upcoming). Used in checkout, multi-step forms, onboarding.
patterns/molecules/stepper.htmlHorizontal or vertical step indicator with three visual states — completed (btn-primary fill + check tick), current (btn-primary fill + teal ring), upcoming (muted + border). 3-6 steps recommended; mode="progress" (default) or "explainer". DM-18 2026-08-22.
Stepper
Storefront molecule (Family 7 — Tier 1)
A horizontal or vertical list of numbered steps. In the default mode="progress" it is a real progress tracker: completed steps carry a filled circle + check tick, the current step is filled + ringed (aria-current="step"), and upcoming steps stay muted (DM-18 2026-08-22 — circles are btn-primary at 5.70:1 AA, not raw teal). mode="explainer" renders neutral numbered steps with no progress state, for marketing 1-2-3 walkthroughs. The D-HOME-P5f/P5g/P5h layout mechanics (flex-1 columns, flex-shrink-0 circles, wrapping text) are still current.
Horizontal (default) — completed tick / current ring / upcoming muted
- BrowseFind events near you
- BuySecure checkout in seconds
- EnjoyShow your ticket at the door
Explainer mode — neutral numbered, no progress state
- BrowseFind events near you
- BuySecure checkout in seconds
- EnjoyShow your ticket at the door
Vertical (mobile)
- BrowseFind events near you
- BuySecure checkout in seconds
- EnjoyShow your ticket at the door
Large size
- BrowseFind events near you
- BuySecure checkout in seconds
- EnjoyShow your ticket at the door
Usage
import { Stepper } from '@epasslive/design-system'
const steps = [
{ id: 's1', label: 'Browse', description: 'Find events near you' },
{ id: 's2', label: 'Buy', description: 'Secure checkout in seconds' },
{ id: 's3', label: 'Enjoy', description: 'Show your ticket at the door' },
]
// Progress tracker (default mode)
<Stepper ariaLabel="How it works" steps={steps} currentIndex={1} />
// Marketing walkthrough — neutral numbered steps, no progress state
<Stepper ariaLabel="How it works" steps={steps} mode="explainer" />
// Clickable steps (function prop — render from a client component)
<Stepper
ariaLabel="Checkout progress"
steps={steps}
currentIndex={1}
onStepClick={(index) => goToStep(index)}
/>Visual structure (DM-18 three-state, 2026-08-22)
┌───────────────────────┐
│ [✓] Browse │ circle: btn-primary fill + check tick (completed)
│ Find events... │ text: min-w-0 flex-1, wraps naturally
└───────────┬───────────┘
━━━ connector: flex-1 h-0.5, teal (this leg is done) — aria-hidden
┌───────────────────────┐
│ [2] Buy (current) │ circle: btn-primary fill + ring-4 ring-ep-teal/20
│ Secure checkout.. │ <li> carries aria-current="step"
└───────────┬───────────┘
━━━ connector: border color (this leg is not done yet)
┌───────────────────────┐
│ [3] Enjoy │ circle: bg-secondary + border, muted number
│ Show your ticket.. │
└───────────────────────┘
Contrast (DM-18): completed/current circles are --ep-btn-primary with an
inverse number/tick — 5.70:1 AA (raw teal was 3.08:1 FAIL). Connectors
stay raw teal: decorative, aria-hidden.
Layout (P5f/P5g/P5h, still current): all columns flex-1 min-w-0 (equal
width); circles flex-shrink-0 (always round); text wraps; connector sits
on the circle's centre line.Modes — progress vs explainer
mode="progress" (default) is for flows where "where am I?" matters — checkouts, wizards, multi-step forms: completed steps tick, the current step rings and carries aria-current="step", future steps are muted (and disabled when clickable). mode="explainer" is for marketing 1-2-3 walkthroughs ("Browse → Buy → Enjoy") that must not read as "you are mid-checkout": neutral numbered steps, no tick, no ring, nothing disabled. See the live "Explainer mode" preview above.
Step count — 3-6 recommended
3-6 steps is the recommendation (NASA Rule 2); outside that range the component logs a dev-only warning — nothing throws in production. Reality today: the production consumer EventEditorNav passes 7 steps for legacy parity (the legacy event editor had 7 sections), so the dev warning fires in dev builds of the dashboard. A bound decision (relax to ≤8 vs restructure the editor nav) is queued for cascade step 6.
Accessibility
The root element is an <ol role="list"> with an aria-label. Each step is an <li role="listitem">; the current step has aria-current="step" (progress mode only). Steps render as <button type="button"> when clickable, with a focus-visible teal ring. Disabled steps have aria-disabled="true" and tabIndex={-1}.
Touch targets — current state (honest): the hit target is the full circle+label row, but --ep-tap-min (44×44, WCAG 2.5.5) is not yet enforced on the step button — at size="sm" (24px circle) targets can fall below 44×44. A min-h/min-w-[var(--ep-tap-min)] fix is queued (cascade step 6).
Known gap (fix queued): step completion is conveyed visually only — the tick and the filled circle both sit inside an aria-hidden="true" span, so screen readers hear the label but not "completed". An sr-only completed announcement is queued (cascade step 6).
Changelog
- DM-18 (2026-08-22): completed/current circles moved from raw teal (3.08:1 — WCAG AA FAIL with inverse glyph) to btn-primary (5.70:1 AA PASS, theme-stable light + dark). Upcoming stays muted + border; connectors stay raw teal (aria-hidden decorative track).
- Explainer mode (2026-08-20):
modeprop added — progress semantics became opt-out for marketing walkthroughs. - D-HOME-P5e (2026-08-11, reverted 2026-08-12): briefly unified all circles to the same teal + shadow-lg. Reverted the next day — a run of identical circles showed no progress at all and regressed the event editor's step bar. Three states restored in D-UI-08.
- P5f (2026-08-11, still current): equal column widths via flex-1 on all <li>
- P5g (2026-08-11, still current): flex-shrink-0 on circles prevents horizontal compression
- P5h (2026-08-11, still current): layout mechanics — text wraps naturally, connector sits on the circle's centre line, items-start alignment
Used by
- HowItWorks —
packages/storefront/src/components/storefront/HowItWorks.tsx(storefront home marketing section; switches tovariant="vertical"below md via matchMedia). Currently runs default progress mode with a staticcurrentIndex={1}; adoptingmode="explainer"is queued (cascade step 6). - EventEditorNav —
packages/storefront/src/components/dashboard/EventEditorNav.tsx(dashboard event editor; clickable steps viaonStepClick+allowJumpAheadfor edit-existing-events,size="sm", 7 steps — see Step count).
Props
steps*StepperStep[]Array of steps. 3-6 recommended — a dev-only warning fires outside that range (NASA Rule 2; production EventEditorNav passes 7 for legacy parity, bound decision queued). Each step has { id: string, label: string, description?: string }.
currentIndex*number0-based index of the current step. Used for aria-current="step" attribute and disabling future steps when onStepClick is set without allowJumpAhead. Ignored for state in mode="explainer".
onStepClick(index: number) => voidOptional click handler. When provided, steps render as buttons. Steps ahead of currentIndex are disabled unless allowJumpAhead is true — right for editors revisiting an existing record, wrong for strict wizards.
ariaLabel*stringAccessible label for the <ol> container. Describes what the steps are tracking (e.g. "How it works", "Checkout progress"). Required — the molecule ships no default strings (i18n by design).
variant"horizontal" | "vertical"horizontalLayout direction. Horizontal is default (desktop), vertical stacks steps top-to-bottom (mobile).
size"sm" | "md" | "lg"mdCircle dimension: sm=h-6/w-6, md=h-8/w-8, lg=h-10/w-10. Note: --ep-tap-min is not yet enforced on clickable steps (fix queued) — sm targets can fall below 44x44.
mode"progress" | "explainer"progress"progress" (default) wears completion/current semantics (tick, ring, aria-current) for checkout-style flows. "explainer" renders neutral numbered steps with no progress state — for marketing 1-2-3 walkthroughs that must not read as "you are mid-checkout".
allowJumpAheadbooleanfalseIf true, users can click steps ahead of currentIndex. Default false is right for wizards where later steps depend on earlier answers.
classNamestringAdditional CSS classes for the <ol> container.
idstringCustom id for the <ol>. Defaults to stepper-{React.useId()}.
Props
Stepper
stepsStep[]—3-6 steps (NASA Rule 2)
currentIndexnumber—0-based
onStepClick(index: number) => void—Synced from source — no interface JSDoc yet; document on `Stepper.tsx`.
ariaLabelstring—Synced from source — no interface JSDoc yet; document on `Stepper.tsx`.
variant"horizontal" | "vertical"'horizontal'Visual variant.
size"sm" | "md" | "lg"'md'Visual size preset.
mode"progress" | "explainer"'progress'Synced from source — no interface JSDoc yet; document on `Stepper.tsx`.
allowJumpAheadbooleanfalseSynced from source — no interface JSDoc yet; document on `Stepper.tsx`.
<Stepper />Accessibility
Type
1 ruleARIA
1 ruleUsage
Import
import { Stepper } from "@epasslive/design-system"Example 1
<Stepper />Example 2
<Stepper variant="default" />Anti-patterns
— Do not hardcode data in molecules
— Avoid complex business logic in molecules