# Spinner The one “working” indicator — a ring, never a shimmer. - Group: Feedback - Import: `import { Spinner } from '@misoto22/folio'` - Page: https://ui.misoto22.com/components/spinner/ - Related: skeleton, progress ## When to reach for it A wait short enough that the shape of what is coming does not matter. Longer than that, use a Skeleton. ## Anatomy - **Live region** (required) — 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. - **Ring** (required) — The inner span — 14px, 18px or 26px of border on a transparent box, and the only element size, tone and className reach. - **Leading quarter** (required) — 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: 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. ## 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. ## Spinner 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. ### Props - `size` — `'sm' | 'md' | 'lg'` default `'md'`. 14px / 18px / 26px. Match the size of the type it sits beside. - `tone` — `'default' | 'current'` default `'default'`. `default` draws the ring against the page — a hairline track in the rule colour with the leading quarter in ink. `current` draws it in the inherited text colour, which is what a spinner inside a filled button needs: on an ink ground an ink ring is invisible. - `className` — `string`. Merged onto the RING, after `size` and `tone`, so it overrides both. - `label` — `string | null` default `'Loading'`. Announced to assistive tech. Pass the specific thing being waited on ("Loading projects"), not the generic word — a screen reader user hearing "Loading" three times cannot tell which three things. Pass `null` for a spinner that sits inside a control which already names the operation (a button whose own label changes to "Saving…"), so the two are not read out twice. Also accepts: `HTMLAttributes`. ## Example — sizes ```tsx import { Spinner } from '@misoto22/folio'
``` ## Example — on a filled ground ```tsx import { Button, Spinner } from '@misoto22/folio'
Indexing 1,204 frames
``` ## Example — which wait shape ```tsx
Unknown duration
Known shape
Known fraction
```