The horizontal pipeline strip both apps kept hand-building: a status circle
per stage, connectors that turn green as stages complete, the stage name under
each circle, and a quiet detail or warning line below it. Promoted under D7
(docs/plan/44-showcase-refinement.md §Rulings) from three near-identical
builds: speedway's WorkflowStageChain stepper band, walmart's
PipelineStepper and the Jobs-row chain. Columns are equal-width and
TOP-aligned deliberately: labels wrap to different heights, and centering the
row puts each circle at a different Y so the connectors zigzag (walmart
learned this in production; the comment travels here).
When to reach for it#
A job or catalog page that walks a record through named stages; a list row
that shows where each job stands; an export flow whose steps a reader may
click into. Read-only by default; pass onStep only when the stages are real
destinations.
Not for wizards that collect input page by page (that is a form flow, see the SchemaForm mode chooser), and not for two-state toggles.
Contract#
steps(required):StepperStep[], each{ key, label, status, detail?, warn?, disabledReason? }.status: "pending" | "active" | "complete" | "blocked".activerenders the ping (suppressed under reduced motion);blockedrenders the paused treatment, never the ping: a job parked in review is not working.detail?: one quiet line (a count, a timestamp).warn?replaces it with the warning ink for the count-that-needs-eyes case ("3 incomplete").disabledReason?: why the step cannot be entered; disables its button and becomes thetitle, so the refusal is polite rather than silent.
aria-label(required): the pipeline's name. A stepper is a landmark.activeKey?: the step whose surface the reader is on; rendersaria-current="step"and the underlined label. Independent ofstatus.onStep?(key): makes every step a button. Omit it and no buttons render.size?: "sm" | "md"(defaultmd):smfits a table row.className: merged last; layout only.
Sanctioned combinations#
- Inside a
Cardwith a title naming the record; the strip is the card's first body row. size="sm"inside a jobs table's expanded row (the walmart Jobs-row shape).warnon the export stage carrying the incomplete count, with the fuller sentence indisabledReasonor the surrounding card.
Banned combinations#
Do not colour labels or connectors through className; the tones are the
contract (success for done, primary for active, warning for blocked). Do not
use onStep to run mutations; a step click navigates, it never submits. Do
not hand-build a stage strip again; this component exists because three
copies drifted.
Before you adopt this#
- Does the shell or a parent already render this? Speedway's job page renders
WorkflowStageChain, whose stepper band this replaces at that app's next deliberate kit upgrade; its note banner and stage list stay local. - Does this app already ship a local implementation? walmart:
PipelineStepper.tsxand the Jobs-row chain; speedway:WorkflowStageChain.tsx(the band only). All named in D7. - Does the app's kit pin reach the version this landed in?
Stepperships in the bump after 0.2.2. - Accessible name and keyboard path: pass
aria-label(required by type); withonStepevery step is a real button, disabled ones expose their reason astitle. - Which canon §A rules bind: doc 12's kicker idiom (labels render through
Kicker, never hand-typed uppercase); doc 01 §A8 rungs for every wash and edge; doc 11 motion (the ping honours reduced motion).
Travels with#
Card (the strip's usual home), Kicker (renders the labels), StatusPill
(a stage list's per-row status, the sibling surface speedway keeps),
Table (the sm strip inside an expanded row).
Snippet#
<Stepper aria-label="Export pipeline" activeKey={tab} onStep={(key) => setTab(key)} steps={[ { key: "scrape", label: "Scrape", status: "complete", detail: "418 rows" }, { key: "map", label: "Schema mapping", status: "active" }, { key: "export", label: "Export", status: "pending", warn: "3 incomplete" }, { key: "submit", label: "Submit", status: "blocked", disabledReason: "Clear review first" }, ]}/>