This component is pure CSS — no JavaScript required.
# Spinner — AGENTS.md
> Auto-generated from `spinner.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.
## Identity
- **Component:** `spinner`
- **Layer:** foundations
- **Status:** stable
- **Last updated:** 2026-05-18
## Summary
- Async operation in progress where the duration is unknown.
## 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.
## Modifier applicability
Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.
| Category | Accepts |
|---|---|
| `background` | — |
| `surface` | — |
| `effect` | — |
| `shader` | — |
| `rhythm` | — |
| `motion` | `none`, `standard` |
| `behavior` | — |
## Anatomy
```html
<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 consumed
### 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 contract
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.
## Guidance
### Do
- Pair every spinner with `role="status"` and a label (or `aria-hidden`
if 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.