Spinner
Indeterminate loading indicator. CSS-only — a single rotating ring, no JavaScript. Used inline next to text, inside buttons during async work, or as a centered indicator inside an empty container.
When to use
- Async operation in progress where the duration is unknown.
- Inline replacement of an icon during a button's
submittingstate. - Foreground indicator over content being refreshed.
For multi-step or skeleton-style loading, prefer Skeleton. For
inline pulsing dots, prefer Dots.
Variants
| Variant | Class | Use for |
|---|---|---|
| Default (m) | .spinner |
Standard size, accent color |
| Small | .spinner.-s |
Inline with body text, button icons |
| Large | .spinner.-l |
Section-level loading |
| Extra large | .spinner.-xl |
Centered page-level loading |
| Inverse | .spinner.-inverse |
On dark / accent backgrounds |
| Muted | .spinner.-muted |
De-emphasized loading |
Modifiers compose: .spinner.-l.-inverse is allowed.
Anatomy
<span class="spinner" role="status" aria-label="Loading"></span>
<!-- Inside a button -->
<button class="button -primary" aria-busy="true">
<span class="spinner -s -inverse" aria-hidden="true"></span>
Submitting…
</button>
<!-- Centered -->
<div style="display:grid; place-items:center; min-height:200px;">
<span class="spinner -xl" role="status" aria-label="Loading"></span>
</div>
Tokens used
From semantic tier
--color-border-muted— track color--color-accent— indicator color--color-ink-inverse— inverse variant indicator--color-ink-subtle— muted variant indicator--size-control-s/-m/-l/-xl— diameter--border-width-focus— small variant thickness--border-width-emphasis— default/large thickness
Component-tier (defined inline)
--spinner-size— overridable diameter--spinner-track— overridable track color--spinner-indicator— overridable indicator color--spinner-thickness— overridable ring thickness
Accessibility
The spinner element carries role="status" and aria-label="Loading"
(authored on the element). Screen readers announce the loading state
when the element appears in the DOM.
Inside a button, the spinner replaces the icon during aria-busy="true"
and should be marked aria-hidden="true"; the button's accessible name
(text or aria-label) carries the meaning. The button's aria-busy
attribute is the source of truth for assistive technology.
The component honors prefers-reduced-motion: reduce by slowing the
rotation from 900ms to 2400ms. It does not stop the animation
entirely because loading without visual feedback is worse for
recognition than a slowed spinner.
Edge cases
- Inside small buttons: the
.-svariant pairs with the standard button size. For very compact buttons, override--spinner-sizelocally. - Dark surfaces:
.-inverseswaps the indicator to ink-inverse; the track stays muted unless--color-border-on-inverseis defined in the theme. - Multiple concurrent spinners: acceptable. The animation has no shared state.
Do
- Pair every spinner with
role="status"and a label (oraria-hiddenif a parent button carries the busy state). - Use the small variant inside buttons.
- Reach for skeletons when loading layout, not just content.
Don't
- Don't animate the spinner manually with JavaScript. The CSS keyframe handles motion.
- Don't disable the reduced-motion accommodation. Slower is fine; no motion at all hides progress.