Activity
The indeterminate wait. It knows only that work is in flight, and says that rather than inventing a fraction.
Overview
An activity indicator is honest about not knowing. That is its whole value, and it is why it is not a worse Progress: the failure this prevents is a bar that advances on a timer, which is a measurement nobody took.
Most of what governs a wait lives here rather than with the determinate bar — when to show one at all, what changes as the wait grows, when to stop describing the wait and start describing the gap, and how to stop. All of it used to sit on a page named “Progress & activity”, which is why the split was worth doing.
Which of the three to reach for is decided on Progress.
An activity indicator may never cover the alert region. A system busy with one patient still has to be able to alarm about another — see what it must never do.
Three shapes, chosen by scope
LINEAR — A REGION
CIRCULAR — A VIEW
4 systems · started 06:41:08
INLINE — A CONTROL
Checking the identifier| Shape | Scope | Rule |
|---|---|---|
| Linear | One region — a panel, a list, a section | Sits at the edge of the region it describes, so its extent says what is waiting. |
| Circular | A whole view, or a large section of one | Centred in the region, with the label centred under it. Never centred over the whole screen on a patient-scoped view — that would cover the alert region. |
| Inline | One control or one row | Sits with the thing it is about. Feedback for a pressed button belongs on that button, per Interaction states. |
Two sizes, not seven. Small for inline use, default everywhere else. A size scale invites choosing by taste; scope already decides the shape, and the shape decides the size.
When to show one, and what changes as the wait grows
A wait is not one state. It is a sequence of them, and the boundaries matter because they are where a clinician's belief changes — first that the system is slow, then that it is broken, then that they should proceed without it. The last of those is the dangerous one, and the interface decides how soon it arrives.
| Elapsed | Show | Why here |
|---|---|---|
| Under ~0.4 s | Nothing | An indicator that appears and vanishes is a flicker. It reads as a fault, and on a list of rows it reads as many faults. |
| ~0.4 – 2 s | The indicator alone | Enough to say the press registered. A label here is noise nobody finishes reading. |
| Beyond ~2 s | Indicator and a label naming the work | The reader has started wondering. "Reading 4 source systems" answers the question they are actually asking, which is what is it doing. |
| Beyond ~5 s | Secondary text: why it is slow, and elapsed time | The question changes to is this stuck. An absolute start time and a running elapsed count answer it without inventing an estimate. |
| Beyond ~10 s | A stated bound, and a way out | Either a real estimate or a timeout with a number on it, plus a control to stop and a route to the record without this result. |
| At the bound | A stated failure, never a continuing spinner | An indicator with no timeout is an unreported error. What replaces it says which source did not answer, per Empty state. |
These are starting points, not measurements — nobody has tested them here, and they are Reasoned claims like most of this system. What is not negotiable is the shape: every wait ends, and the reader is told more as it lengthens rather than less.
When it is slow and the duration is genuinely unknown
The usual advice — past ten seconds, use a progress indicator — is not an answer. A progress indicator needs a computable fraction, and if one existed you would already be showing it. Indeterminate and long is the common case, and switching component does not make the number appear.
Past the bound, stop describing the wait and start describing the state: which sources have answered, which have not, what can be shown now, and what stopping costs.
- Observations · Medication · Results answered
- Theatre record no response since 06:41:08
A brief without the theatre record will say so on every page it is printed on.
- Name what is outstanding. "Waiting on the theatre record" is actionable — somebody can phone theatre. "Loading" is not.
- Offer the partial result explicitly, with its gap stated. Never render it silently as though complete: that is the omission failure Composed output exists to prevent, and the gap must travel with the artefact.
- There is always a way to stop. A wait a clinician cannot end is a wait they will end by leaving, and leaving loses the three sources that did answer.
- Stopping is recorded, not just permitted. A brief assembled from three of four sources is a different object from one assembled from four, and the record says which.
Stopping
- Anything past the bound is cancellable, and the control says what cancelling does — whether partial work is kept or discarded.
- Cancelling is not failing. It is a decision, recorded as one, and it never produces an error state that implies the software went wrong.
- Navigating away is a cancellation. If work continues in the background it says so and reports its outcome somewhere the clinician will see it, per Pending & unreviewed work. Work that completes into a screen nobody is looking at has not completed.
- A stop control never sits where the primary action was. A button that becomes Cancel under a finger already moving is a cancellation nobody chose.
What the component enforces, and what it only asks for
Short table, because an indeterminate indicator is mostly a refusal to claim things.
| Rule | Held by | How |
|---|---|---|
| It cannot claim to know how long | The component | There is no value on the type at all.Asserted: Activity: no value, and none can be passed |
| It names the work | The component | label is required, so an unlabelled spinner is not expressible.Asserted: Activity: an unlabelled indicator cannot be rendered |
| It is announced as busy, not as a false zero | The component | No aria-valuenow; the spinner is aria-hidden and the region is aria-busy.Asserted: Activity: circular and inline carry no progressbar role |
| Movement stops under reduced motion | The component | The animation is suppressed rather than slowed, because movement is the alarm channel. Asserted: Activity: movement stops rather than slows under reduced motion |
| An indeterminate bar cannot be read as a fraction | The component | Hatched across the whole track. It was a third of the track — “a third done” — and under reduced motion the full track, the picture of “finished”. Asserted: Activity: an indeterminate bar does not look like a fraction |
| There is a bound, and it resolves to a stated failure | You | The component has no timer. A region that spins for ever is a request that died, rendered as one still working. |
| It does not cover the alert region | You | A layout decision, and the rule that overrides every other one on this page. |
A rule stated on a component page reads as a rule the component keeps. 4 of these 6 are; 2 are obligations this page was quietly handing to the reader. The two rules that make an indeterminate wait safe — that it ends, and that it never covers an alarm — are both yours.
Do's and don'ts
Assembling the handover brief
Pathology is slow this
morning · started 06:41:08 · 00:12 elapsed
Past five seconds the question stops being what and becomes is this stuck. An elapsed count answers it without inventing an estimate.
Assembling the handover brief
This may take a
moment…
A guess presented as information. Thirty seconds later it is still saying "a moment", and the reader has stopped believing anything on the screen.
Prior studies
Linear, at the edge of the one region that is waiting. Everything else on the screen stays readable and stays alarmable.
One slow archive has taken the whole view. The patient header, the results already on screen and any alarm arriving are all behind this.
Checking the identifier
Inline, beside the field it is about. The rest of the form stays usable while it resolves.
A 1.5 rem spinner centred over the form for 300 ms.
Below half a second an indicator is a flicker that reads as a fault — and repeated down a list of rows, it reads as many faults.
Waiting on the theatre record — no response since
06:41:08.
Show the brief without it ·
Stop waiting
Past the bound the wait stops being the subject. Somebody can phone theatre, or proceed with the gap stated.
Still loading… (at 00:34, unchanged since 00:02)
Three sources answered and this says nothing about any of them. There is no way to proceed and no way to stop, so the clinician leaves — losing the three.
Accessibility
- No
aria-valuenow. An indeterminate indicator is announced as busy, not as a false zero — a zero is a measurement. - The spinner is
aria-hidden, the region carriesaria-busy="true", and the meaning is in a text label beside it. - The label names the work — “Searching prior studies”, never “Loading”, which is true of every wait ever rendered.
- Under
prefers-reduced-motionthe animation stops and the bar holds its full width. Movement is the alarm channel; a decorative spin competes with it.
Outcomes of use
- Effectiveness — the reader can tell working from stuck from failed, which is the whole job of an indeterminate indicator.
- Efficiency — a decision to wait or to proceed with the gap stated, taken at the moment the elapsed count makes it answerable.
- Satisfaction — not being lied to. Attributes believed to contribute to an outcome; the outcome is settled by observing real use — 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 bound that resolves to a stated failure. (you) Mitigates: a permanently spinning region read as “still working” when the request died.
- The timeout wording states it is not a result. (you) Mitigates: a failed query read as an empty one — see Empty state.
- Anything past the bound is cancellable, and cancelling is recorded as a decision. (you) Mitigates: a clinician trapped by a wait they cannot end.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/activity.json
<Activity
label="Searching prior studies"
shape="circular" // linear | circular | inline
startedAt="14:27:02"
/>
// There is no value on this type at all, so an indicator that does not
// know how long cannot accidentally claim that it does. The bound, and
// what happens when it is reached, are the caller's.
Related
- Progress — when the fraction is genuinely known.
- Skeleton — holding a slot rather than spinning in it.
- Empty state — a wait that ended in nothing, and why that is not the same as failing.
- Motion — why movement is reserved.