Build / Atoms
Stepper
Progress through a multi-step flow.
Built for the booking wizard, where each step is its own route
(purpose → availability → details → booked) so a booking can be linked
or resumed at any stage.
Completed steps are navigable; upcoming ones are inert. State is conveyed
with text and aria-current, not only the marker's color, and the whole
list is a <nav> with an ordered list so the sequence is announced.
A step marked terminal is complete on arrival — the end of a flow asks
for nothing, so it shows a check rather than a number it will never move
past. It stays inert, since there is no step after it to return from.
import { Stepper } from '@meetcard/deck'Example
<Stepper currentStepId="availability" steps={[ { id: 'purpose', label: 'Purpose', href: './purpose' }, { id: 'availability', label: 'Time' }, { id: 'details', label: 'Details' }, { id: 'booked', label: 'Confirmed', terminal: true }, ]} />Stories
Source
const Default = () => <div style={{ maxWidth: 480 }}>
<Stepper
steps={[
{ id: 'purpose', label: 'Purpose' },
{ id: 'availability', label: 'Time' },
{ id: 'details', label: 'Details' },
{ id: 'booked', label: 'Confirmed' },
]}
currentStepId="availability"
onSelect={fn()} />
</div>;Source
const FirstStep = () => <div style={{ maxWidth: 480 }}>
<Stepper
steps={[
{ id: 'purpose', label: 'Purpose' },
{ id: 'availability', label: 'Time' },
{ id: 'details', label: 'Details' },
{ id: 'booked', label: 'Confirmed' },
]}
currentStepId="purpose"
onSelect={fn()} />
</div>;Source
const Complete = () => <div style={{ maxWidth: 480 }}>
<Stepper
steps={[
{ id: 'purpose', label: 'Purpose' },
{ id: 'availability', label: 'Time' },
{ id: 'details', label: 'Details' },
{ id: 'booked', label: 'Confirmed' },
]}
currentStepId="booked"
onSelect={fn()} />
</div>;A step marked terminal is complete the moment it is reached. The marker
settles from white through the success tint to the filled disc, drawing its
check — the resting style is the finished one, so with motion off the same
check is simply already there.
Source
const TerminalStep = () => <div style={{ maxWidth: 480 }}>
<Stepper
steps={bookingSteps.map((step) =>
step.id === 'booked' ? { ...step, terminal: true } : step,
)}
currentStepId="booked"
onSelect={fn()} />
</div>;Phone. Every step keeps its label, stacked under its own marker — a row of bare numbers tells you where you are and nothing about what it is out of, which is most of the point of a progress indicator.
Five steps is the widest the booking flow gets (Book with Team adds a
Team step), so it is the case worth pinning.
Source
const Mobile = () => <Stepper
steps={[
{ id: 'purpose', label: 'Purpose' },
{ id: 'availability', label: 'Availability' },
{ id: 'team', label: 'Team' },
{ id: 'details', label: 'Details' },
{ id: 'booked', label: 'Booked', terminal: true },
]}
currentStepId="availability"
onSelect={fn()} />;With href, steps render as links so each is a real, shareable route.
Source
const AsLinks = () => <div style={{ maxWidth: 480 }}>
<Stepper
steps={bookingSteps.map((step) => ({
...step,
href: `/ben/book/${step.id}`,
}))}
currentStepId="details"
onSelect={undefined} />
</div>;The same flow at every stage, for checking each state at once.
Source
const EveryStage = (args) => (
<Stack gap={24} style={{ maxWidth: 480 }}>
{/* Each nav landmark needs a unique accessible name when several
appear on one page — hence the per-stepper `label`. */}
{bookingSteps.map((step) => (
<Stepper
{...args}
key={step.id}
currentStepId={step.id}
label={`Progress: ${step.id}`}
/>
))}
</Stack>
);Props
| Prop | Type | Default | Description |
|---|---|---|---|
currentStepIdrequired | string | — | id of the step in progress. |
stepsrequired | Step[] | — | — |
label | string | 'Progress' | Accessible name for the progress nav. |
onSelect | (stepId: string) => void | — | Called when a completed step is activated, for non-link navigation. |
Also accepts everything from Omit, so id, data-* and event handlers pass straight through.