Feedback
Spinner
The one “working” indicator — a ring, never a shimmer.
Usage
When to reach for it
import { Spinner } from '@misoto22/design'Notes
The system's one "working" indicator.
A ring, not a blurred glow and not a shimmer: the White Reset has no light source, so depth and softness are not available to it. The leading quarter is the only thing that distinguishes the ring from a plain circle, which is why the track stays a hairline — a spinner is a hint that time is passing, not a feature of the page.
It spins under motion-safe only. A reader who has asked for less motion gets a static ring, which still reads as "not finished" because the leading quarter is darker than the track.
For a wait long enough that the reader would otherwise wonder whether the page is broken, prefer Skeleton — a shape that describes what is coming beats a dot that describes nothing.
className reaches the RING, alongside size and tone, and overrides them: every utility a caller has for a spinner is about the ring, and merged onto the wrapper instead className="size-8" grew an invisible box around an unchanged 18px circle. Layout still works from there — the wrapper is inline-flex and takes the ring's margin box as its own.
Anatomy
| Element | Description |
|---|---|
| Live regionrequired | The outer span, and the only part with a voice. It carries role="status" while there is a label; pass label={null} and it turns into an aria-hidden box with no role at all. |
| Ringrequired | The inner span — 14px, 18px or 26px of border on a transparent box, and the only element size, tone and className reach. |
| Leading quarterrequired | border-t, drawn in --ink at the default tone and in the inherited colour at current. It is the whole difference between a ring and a plain circle, which is why a still ring still reads as unfinished. |
| Screen-reader label | An sr-only span holding label, present unless label is null. Announced once, when the spinner mounts, and never again. |
Best practices
Do
- Pass tone="current" for a spinner on any filled ground: the default draws the leading quarter in --ink over a --rule-2 track, and inside a primary Button both of those are the ground it is sitting on.
- Announce the arrival somewhere else. The label is read once on mount and nothing is said on the way out, so a reader who heard “Loading projects” is never told the projects came.
- Reach for label={null} only inside a control that already names the operation — it hides the whole element from assistive tech rather than merely dropping the text, so a silenced spinner standing on its own is a wait nobody is told about.
Don’t
- label defaults to the bare word “Loading”, so a Spinner written without the prop ships the exact announcement the prop exists to prevent — the default is a placeholder, not a value.
- className is merged onto the ring AFTER size and tone, so it beats both: <Spinner size="lg" className="size-4" /> is a 16px ring, and the prop whose whole job was naming the size is the one that lost.
- Nothing here sets aria-busy — Button does that for its own control — so a spinner laid over a panel leaves the panel announced as ready while its contents are stale and its buttons still take clicks.
Examples
sizes
The three rings — 14px, 18px and 26px — matched to the type they sit beside rather than picked for emphasis. size is the only thing that resizes one: className lands on the outer wrapper, so a size utility passed that way grows an empty box around an unchanged ring. label defaults to the bare word Loading, which is the announcement the prop exists to prevent — name the specific wait, because three spinners all saying Loading tell a screen reader nothing, and each is read once on mount and never again.
on a filled ground
Two spinners standing on a ground that is not paper. The default tone draws the leading quarter in --ink over a --rule-2 track, and inside a filled control both of those are the ground it is sitting on — so pass tone current and let the ring inherit the text colour. label null belongs to exactly this case: the control already names the operation, and passing it hides the whole element from assistive tech rather than merely dropping the text, so a silenced spinner standing on its own is a wait nobody is told about.
which wait shape
Three waits, three shapes, and the question each one answers. A Spinner is for a wait whose length nobody knows and whose result has no shape worth promising. A Skeleton is for a wait where the shape is known — it holds the layout so the page does not jump when the records land. A Progress is for a wait with a real fraction, and only a real one: a bar filled from an invented estimate is a spinner that lies. Reaching for the spinner in all three cases is the usual failure, and it is the one of the three that says least.
Unknown duration
Checking the deployKnown shape
Known fraction
Accessibility
- label names the specific thing being waited on; three spinners all saying “Loading” tell a screen reader nothing.
- label={null} silences it for use inside a control that already announces the operation.
- Spins under motion-safe only; the static ring still reads as unfinished because the leading quarter is darker.