Stepper
Where somebody is in a flow with a known number of stages.
- Select studiesCompleted
- De-identifyCompleted
- Upload DICOMCurrent step
- Submit to ACRNot started
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/stepper.jsonUsage
import { Stepper, StepperStep } from "@/components/ui/stepper"<Stepper value={2} aria-label="ACR submission progress">
<StepperStep label="Select studies" />
<StepperStep label="De-identify" />
<StepperStep label="Upload DICOM" />
<StepperStep label="Submit to ACR" />
</Stepper>value is the index of the current step. Everything before it is complete, everything after it is
not started, and no step is told which it is — the root works it out, so advancing a wizard is one
number changing rather than four props.
Vertical
Down the side of a panel, where each stage has room for a line of detail. This is the arrangement to reach for when the labels are longer than two words, because a horizontal row of long labels either wraps or truncates.
- DemographicsName, NHS number, date of birthCompleted
- ConsentSigned 9 March, awaiting countersignatureCurrent step
- Prior imagingLink studies held at the referring trustNot started
- Assign clinicianNot startedNot started
Clickable steps
Pass onStepChange and every step becomes a real <button>: focusable, in the tab sequence,
operable by Enter and Space. Without it the steps are plain elements and
nothing stops on them.
Disable the steps that are not reachable yet. A radiologist cannot sign a report they have not reviewed, and rendering the button anyway invites them to try.
Do not make steps clickable and then have the click do nothing but move a highlight. If pressing "Peer review" does not take the reader back to the review form, it should not be a button — make it static instead. Interactivity that is only decorative is worse than no interactivity, because it costs a tab stop and promises something it will not do.
A step that failed
state overrides what the index implies, which is what makes an error visible. The flow has not
advanced — the current step is still where the work is — but the stage that rejected is drawn in
urgent, and the message says what went wrong rather than only that something did.
- Select studies14 studiesCompleted
- De-identifyCompleteCompleted
- Upload DICOM3 of 14 rejected — burnt-in identifiersNeeds attention
- Submit to ACRNot started
Sizes
Three, from the control scale — 24, 32 and 40 pixel circles.
- De-identifyCompleted
- Upload DICOMCurrent step
- Submit to ACRNot started
- De-identifyCompleted
- Upload DICOMCurrent step
- Submit to ACRNot started
- De-identifyCompleted
- Upload DICOMCurrent step
- Submit to ACRNot started
| Size | Use |
|---|---|
sm | inside a modal header, or a side panel with little room |
md | the default; the top of a wizard or a form page |
lg | a full-page flow where the stepper is the main orientation |
Accessibility
The stepper renders as an ordered list — <ol role="list"> with an <li> per step — and not as a
tablist. That is the decision worth explaining, because the two look alike and behave nothing
alike:
- A tablist is a set of peers, all reachable, in any order. Its pattern is one tab stop for the whole set and arrow keys to move within it.
- A stepper is an ordered progression, most of it not reachable yet. There is no set of peers to arrow between, so borrowing the tablist pattern would announce a structure that does not exist and take away the ordinal position, which is the one thing the reader wants: "list, 4 items, item 3 of 4".
role="list" is restated on the <ol> on purpose. Safari drops list semantics from a list whose
list-style is none, which is every styled stepper there has ever been.
The current step carries aria-current="step". It sits on the <li> in the static case and on the
<button> in the clickable one, because it belongs on whichever element represents the step to the
reader.
Static steps are not focusable. There is no tabIndex on them and there is no handler; Tab
skips the whole list. A tab stop that does nothing when pressed is a bug, and giving a decorative
step one is the usual way this component goes wrong.
Clickable steps are ordinary buttons in ordinary document order, so Tab and
Shift + Tab move between them and Enter or Space
activates. Arrow keys are deliberately left alone — capturing them would make the steps behave like
a tablist they are not. A step that cannot be reached yet is a disabled button rather than a
dimmed <div>, so it is announced as unavailable instead of simply being absent.
The circle is aria-hidden: a tick, a number and a warning triangle are three shapes that mean
nothing read aloud. Each step carries a visually hidden word for its state instead, so the
announcement is "Completed, De-identify" rather than "De-identify" in a colour nobody can hear.
STEP_STATE_LABEL exports that wording, so a tooltip or a summary line can reuse it rather than
retype it.
API Reference
Stepper
Prop
Type
Give the root an aria-label — "ACR submission progress" — so the list is announced as something
rather than as a list of four.
StepperStep
Prop
Type