Progress
A determinate bar. It knows how much is done and says so, in the units the work is counted in.
Overview
Progress is the determinate half of a wait: the work has countable units and the interface can say how many are done. Where the count is not known, the honest control is Activity, and using it is not a lesser answer — inventing a fraction to look reassuring is.
Progress, Activity, Skeleton and PendingValue were documented together under the title
“Progress & activity”, and the code shipped as one item. The code was split
first and the page was left, which was the wrong way round: most of what was on it —
the shapes, the thresholds, the long wait, cancelling — was about the indeterminate
case, filed under the determinate one's name. Three pages now, one per item.
Which to use
| Situation | Use | Why |
|---|---|---|
| Loading a list or a record | Skeleton | Shows the shape of what is coming and reserves its space, preventing layout shift — see Motion. |
| Analysing a recording, uploading, exporting | Progress (determinate) | Steps or bytes are countable. Say how far. |
| A query of unknown duration | Activity (indeterminate) | Honest about not knowing. |
| A button awaiting a server | Pending state on the button | Feedback belongs on the control that was pressed — see Interaction states. |
| Background work the clinician need not wait for | Status chip | Persistent, non-interruptive — see Status chip. |
The table above has always named these separately, and until recently they were one
component with an optional value. That conflation produced both of the defects
the split removes. max defaulted to 100, so a bar for seven of twelve leads
rendered 7 % — a figure nobody computed, from a denominator
nobody supplied. And the circular and inline shapes returned before reading
value at all, so a determinate value passed with either was silently discarded.
Progress now requires value, max and
unit and has no shape; Activity has no value on its type.
Anatomy
| Element | Rule |
|---|---|
| Label | Names the work in clinical terms. "Analysing 12-lead ECG", not "Loading…". |
| Countable state | Where determinate — steps, leads, bytes. Never a bare percentage where a unit exists. |
| Started at | Absolute time. Lets a clinician judge whether something has stalled. |
| Bound | Estimated remaining, or the timeout. Every indicator states one or the other. |
What it must never do
Full-screen loading states, blocking overlays and progress modals are prohibited on patient-scoped screens. A busy system must still be able to alarm — see Elevation, where the alarm layer sits above everything. Work in progress occupies the region it belongs to and nothing more.
- Never fabricate progress. A bar that advances on a timer rather than on actual work is a lie that a clinician will calibrate against and be misled by.
- Never restart or loop a determinate bar. A bar that reaches 90 % and returns to 10 % destroys the only thing it was for.
- Never leave a spinner running indefinitely. Every indeterminate indicator carries a timeout, after which it becomes a stated failure with a retry.
- Never use an alarm hue. Progress is teal; it is not a clinical signal.
- Never animate a waveform as it loads. A trace drawing itself cannot be measured and implies signal change — see Motion.
- Never hide what is already usable. If three of four panels have loaded, show three; do not withhold them until the fourth arrives.
Named work, countable units, real progress. A clinician can decide whether to wait.
Unnamed, unbounded, uninformative — and if this covered the screen it would also be hiding any alarm that arrived.
Formatting
- Placement. Centred in the region it describes, label and secondary text centred to it. Inline indicators sit with their control, at its leading edge.
- The label names the work in clinical terms — "Assembling the handover brief", not "Loading". "Loading" is true of every wait ever rendered and therefore tells the reader nothing.
- Secondary text carries time, not reassurance. An absolute start and a running elapsed count. Not "this may take a moment", which is a guess presented as information.
- Right-to-left. The layout mirrors — label, indicator and secondary text swap edge, and a linear track fills from the other side. The circular indicator does not reverse its spin. Its rotation carries no directional meaning, so mirroring it would be motion for its own sake.
- Contrast. An indicator on a filled surface uses the foreground paired with that surface, never a fixed grey that happens to work on white.
What the component enforces, and what it only asks for
The fraction is held; what the fraction is counting is not.
| Rule | Held by | How |
|---|---|---|
| A bar cannot be shown without its denominator | The component | value, max and unit are all required. max used to default to 100, which turned 7 of 12 into “7 %”.Asserted: Progress: the count is shown, not only a percentage |
| The count is shown, not only a percentage | The component | “7 of 12 leads” renders as text and as aria-valuetext.Asserted: Progress: the count is shown, not only a percentage |
| There is no shape that discards the value | The component | No shape prop. The circular and inline forms had nowhere to put a value and silently dropped it; they belong to Activity.Asserted: Progress has no shape that could discard its value |
| The denominator does not change mid-flight | You | You pass max on every render. Nothing here notices it grew. |
| Completion means success | You | A full bar is a full bar. Whether the work succeeded, and whether part of it failed quietly, is yours to say beside it. |
| The last step is counted | You | If the write to the record is not in max, the bar sits full while it happens. |
A rule stated on a component page reads as a rule the component keeps. 3 of these 6 are; 3 are obligations this page was quietly handing to the reader. Every way a fraction can be true and still mislead is on your side of this table.
Do's and don'ts
The determinate case. The other pairs moved to Activity and Skeleton.
Named work, countable units, real progress. The clinician can decide whether to wait.
Unnamed, unbounded, uninformative — and full-screen it would be hiding any alarm that arrived.
The pairs below are about the determinate bar specifically: what a fraction claims, and the ways a fraction can be true and still mislead. The indeterminate cases are on Activity.
Finished with 3 leads unreadable. V4–V6 excluded — anterior interpretation limited.
The bar reached the end and the work did not succeed. Those are two different facts and the bar can only carry one of them.
Complete
Twelve counted, nine analysed. A clinician reads a full bar as a finished job and takes the interpretation at face value, including the part of the heart nobody looked at.
Scope known before the count started.
The denominator was settled first, so the fraction means the same thing at the end as it did at the beginning.
… a moment later: 7 of 40 studies
The set grew as it was read, so a bar that looked nearly done is now a fifth of the way. The clinician who decided to wait made that decision on a number that no longer exists.
Writing to the record…
The commit is a step and it is counted. The bar is at four fifths because one fifth of the work has not happened yet.
(still writing to the record)
Full for eleven seconds while the write completes. The reader concludes it is either finished or stuck, and both conclusions are wrong — one of them navigates away mid-write.
Connection dropped at series 8. Resuming from 7 — nothing already sent is being sent again.
A retry keeps the count and says what happened. Progress that stalls and explains itself is still progress.
… it was at 7 of 12 a second ago
The bar went backwards with no explanation. A clinician watching a transfer to another site cannot tell a retry from data being lost, and the second reading is the one that gets acted on.
Continues in the background. Named wherever it is shown.
Work that outlives the screen it started on carries the patient it belongs to, so it cannot be read as belonging to whoever is on screen now.
Now viewing: OKAFOR, C. · MRN 51 330 264
The export is for the previous patient and the header is for this one. Nothing on the bar says so, and the interface has put one patient's work under another patient's name.
Accessibility
role="progressbar"witharia-valuenow,aria-valueminandaria-valuemax, and anaria-valuetextthat reads “7 of 12 leads” rather than a percentage.- The accessible name carries the countable state, so a screen-reader user gets what a sighted reader gets from the bar.
- Announce at intervals, not continuously. A live region updating every frame is unusable; update on meaningful steps.
- Under
prefers-reduced-motionthe fill still moves, because its position is the information. Nothing else animates.
Outcomes of use
- Effectiveness — a clinician correctly distinguishes “still working” from “finished with nothing” from “failed”.
- Efficiency — an informed wait-or-move-on decision. A bar with a remaining estimate is the difference between waiting eight seconds and abandoning the screen.
- Satisfaction — perceived responsiveness, which is not the same as speed. These are attributes believed to contribute to an outcome; the outcome is settled by observing real use, not by this page — see Usability & context of use.
Clinical safety notes
Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1). Which of these the component keeps and which are yours is stated in the guarantees table; a control the component does not keep is one your risk file must show you keep.
- Never covers the alert region; no blocking overlays. (you) Mitigates: an alarm obscured while the system is busy.
- Every indeterminate indicator has a timeout that resolves to a stated failure. (you) Mitigates: a permanently spinning region read as "still working" when the request died.
- No fabricated progress. (the component) Mitigates: a clinician calibrating their waiting on a fiction.
- Timeout wording states it is not a result. (you) Mitigates: a failed lookup read as "no prior study exists".
- Partial results shown as they arrive. (you) Mitigates: usable data withheld behind a slower request.
- No alarm hues. (the component) Mitigates: progress being read as clinical priority.
- Waveforms never animate while loading. (you) Mitigates: measurement from a transitional render.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/progress.json
<Progress
label="Analysing 12-lead ECG"
value={7} max={12} unit="leads" // all three required
startedAt="14:26:41"
/>
// No shape prop: a determinate value has to be shown somewhere, and the
// circular and inline forms had nowhere to put it — they used to discard
// it silently. Those shapes belong to Activity.
Related
- Activity — the same wait, when the fraction is not known.
- Skeleton — holding the shape of what is coming.
- Button — a control awaiting a server states it on itself.
- Status chip — background work nobody is waiting on.