User Interface (UI)
Let's start with how a spinner looks: its parts, its size and colour, how it moves and the one variable you can set.
Anatomy
- Container — The
<svg>, a square that turns as a whole, without taking up any more room. - Track — A faint full circle, which shows the path the indicator follows.
- Indicator — A quarter of the circle at full strength, which turns around the track.
Every spinner has all three parts, and nothing else. The indicator is what people watch, so it's the part that has to stand out (see Contrast), while the track stays faint. We recommend drawing both as strokes in one <svg>, with a 16 × 16 viewBox and a track at 25% opacity. The parts always render in the same order, which you can see in the DOM contract.
Size and colour
The Spinner takes its size and colour from where it sits, just like an icon. It's 1em wide, the size of the text around it, and it's drawn in currentColor, the colour of that text. Next to a label, it lines up without any setup, and to give it another size, you can set --spinner--size from a class.
Saving draft
Loading results
import styles from "./Loading.module.css";
export function Loading() {
return (
<>
<p>
<Spinner /> Saving draft
</p>
<h2>
<Spinner /> Loading results
</h2>
<Spinner className={styles.large} />
</>
);
}
.large {
--spinner--size: 2rem;
}
Inside a component, the component sets the size. In a Button, the Spinner is as big as the Button's icons, with --button--icon--size. A spinner on its own, in the middle of a panel that's loading, is where a larger size comes in.
Keep in mind that these examples only show sizes. On a real page, the text that says what's loading goes in a status message, so screen readers hear it too, as under Say what's loading.
States
The Spinner can't be hovered, focused or pressed, so it has no interactive states. It's in the page while something's busy and gone once it's done, and while it's there, it moves in one of two ways:
| State | Selector | Recommended look |
|---|---|---|
| Turning | .spinner | One full turn every 0.8 seconds, at a steady speed |
| Reduced motion | @media (prefers-reduced-motion: reduce) | No turning. The whole spinner fades to 40% and back every 1.6 seconds |
You'll find why it pulses under Motion. The wait it shows has states of its own, like done and failed, which belong to the page, and you'll find those under Every state.
Tokens
The Spinner doesn't read any theme tokens. Its colour is currentColor, so it follows whatever colour, theme or colour scheme the text around it follows. It has a single variable of its own:
| Variable | Default | Set by |
|---|---|---|
--spinner--size | 1em | Components that place it, like the Button with --button--icon--size, and your classes |
The Spinner's styles sit in the components cascade layer, so a class of yours that sets --spinner--size wins without any specificity tricks, as in the example above. To change its colour, set color on it, or on the text around it.
UI Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
spinner/em-sized | Should | Component | It's 1em by default, so it matches the text around it, and --spinner--size sets any other size | Open Components | Review |
spinner/current-color | Must | Component | It's drawn in currentColor, so it follows the text around it, every colour scheme and forced colours mode | Open Components | Emulation |
spinner/contrast | Must | Both | The indicator has a contrast of 3:1 against what's behind it | WCAG 1.4.11 (AA) | Contrast checker |
spinner/reduced-motion | Must | Component | It doesn't turn when prefers-reduced-motion: reduce is set, but still shows that something's happening, like with a pulse | WCAG 2.3.3 (AAA), Open Components | Emulation |
spinner/stable-layout | Must | Both | It keeps a fixed size while it turns, and showing or hiding it doesn't move anything around it | Open Components | Visual regression test |