spinner
A spinner says “working on it” when you can't say how long the wait will be. It is a faint square with a gap running round it: the one moving thing on an otherwise still screen.
when to use
- For a short wait of unknown length in a small part of the screen: one value, a count, the content of a panel.
- In place of a value while it loads, with a name, read out by screen readers, that says which value is loading.
- Beside a short line of text that says what is loading.
when to use something else
- For an action someone started. Use the busy state of the button, or of the icon button. The spinner then sits inside the control that was pressed, and the button ignores a second press.
- While a table's rows load. Use the loading state of the data table. It marks the table as busy and runs a thin line along its top edge, so the wait shows where the rows will be.
- For the result. When the work ends, replace the spinner with what happened: the content itself, a toast for a background job that finished, or a banner for a failure.
- For long work whose progress is known. Today there is no progress bar. Say the progress in a line of text next to the spinner: “12 of 40 copied”.
anatomy
- Frame. A small square outline with square corners, in blue unless it is given another colour. It is drawn faintly, it is always there, and it never moves.
- Lit part. Three quarters of the frame, in full colour, with square ends. It starts in the middle of the top edge, so no corner ever shows a notch.
- Gap. The quarter that isn't lit. It runs round the frame; nothing else moves.
The line gets a little thicker as the spinner gets bigger.
variants and states
A spinner can be any size, and its line grows with it. Buttons hold small ones, and the same small size sits well beside a line of small text. On its own, a spinner is a little bigger, and a spinner for a whole panel bigger still. The exact sizes are in the specs.
A spinner is blue by default, and on its own it doesn't take on the colour of the text around it. Inside a button, it takes the colour of the button's label, so it still shows on the emphasized button's blue fill. Anywhere else it can take any colour: one of the system's colour tokens, such as the muted text colour, or the colour of the text around it.
In place of a value, the spinner's name says what is loading.
behaviour
- The frame stays still while the gap runs round it at a slow, steady pace, without end. The spinner takes no focus.
- With reduced motion on, it stands still, three quarters lit, and its name carries the state.
- It never stops on its own. Remove it when the work ends.
What to show while something loads, and when, is described in loading and progress.
The full keyboard map is on the spinner's React page.
accessibility
- A screen reader knows a spinner as a status message, named “loading” unless it is given a name of its own.
- Name the thing that loads, such as “loading volumes”, rather than keeping the general “loading”.
- Beside visible text that says the same thing, the name is read twice. Hide the spinner from assistive technology, and let the text speak.
- With reduced motion on, nothing moves, so the words carry the state: the spinner's name, or the text beside it.
- The lit part stands out from every background with a contrast of at least 3 to 1, and the default blue with at least 6.8 to 1; the faint frame is only decoration. A colour of your own must keep 3 to 1.
Today:
- The spinner's name isn't text on the page, and a live region announces changes to its text. A spinner that is added to the page carries no text, so most screen readers won't announce it appearing. When the wait matters, put a short message in a live region that is already on the page, and change its text.
content
- The name says what is loading, with no ellipsis: “loading volumes”, “checking storage-01”. It is read out, not shown.
- Text beside it says the same words, in sentence case: “Loading volumes”. No “please wait”.
- When a wait runs past a few seconds, say more: what is happening, and whether people can leave the page in the meantime.
do and don't
related
- Button: the busy state, for an action in progress.
- Data table: its loading state, while the rows load.
- Loading and progress: what to show while something loads, and where the spinner fits.
- Message handling: what to show once the wait is over.
specs
| size | where | line |
|---|---|---|
| 12px | In a small button | 2px |
| 14px | In a medium or a large button, and beside 13px text | 2px |
| 16px (the default) | On its own | 2px |
| 24 to 40px | For a panel | 2px up to 24px, 3px at 40px |
| setting | in code | default |
|---|---|---|
| Size | size, in px; the frame is size × size |
16 |
| Colour | color, any CSS colour: a token such as var(--fg-muted), or currentColor to follow the text around it |
--cloud-blue, set by the spinner's own CSS |
| Name | label, which becomes the aria-label of the role="status" element |
loading |
| Hidden beside text | aria-hidden="true" |
Not hidden |
- Frame: one square path the spinner's size, at 20% opacity (
stroke-opacity: 0.2). It never moves. - Lit part: the same path drawn again, as a dash three quarters (75%) of the frame long, with square ends (
stroke-linecap: butt) and sharp corners (miter). The path starts in the middle of the top edge. - Gap: the other quarter (25%) of the dash pattern, which travels round the frame.
- Line:
max(2, round(size / 16))px: 2px up to 24px, then a pixel more for every 16px, so 3px at 40px. - Motion: the dash's offset moves by the frame's length, one lap in 4s,
linear, without end. Underprefers-reduced-motion: reducethere is no animation. - In a button: a 12px spinner in a small button and 14px in a medium or large one, with
color: inherit, so it takes the label's colour. In an icon button it takes the button's colour. - Contrast: the lit part at least 3:1 against every background, and
--cloud-blueat least 6.8:1. The faint frame is decoration. A custom colour must keep 3:1. - Focus: none.