Spinner
Spinner — the indeterminate busy indicator.
Examples
Checking availability…
<p class="spinner-status" role="status">
<span class="spinner spinner-sm" aria-hidden="true"></span>
Checking availability…
</p>A spinning circle says nothing. The live region has to exist in the DOM before it has content — a role="status" inserted at the same moment as its text is frequently not announced at all, because there was no region for the platform to be watching.
<div id="results" aria-busy="true">
<span class="spinner" aria-hidden="true"></span>
</div>When the wait replaces content in place, aria-busy on the region is the right signal and the spinner is decoration.
Tokens 5
Level 2, declared on .spinner itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --spinner-color | currentColor |
| --spinner-duration | 700ms |
| --spinner-size | 1.5rem |
| --spinner-thickness | calc(var(--spinner-size) / 8) |
| --spinner-track-color | color-mix(in oklab, var(--spinner-color) 22%, transparent) |
Variants and states
Variants
.spinner-lg.spinner-sm
Inside it
.spinner-status
State it reads
None.
Before you ship it
What you have to do 4 requirements
- A spinning circle is not an accessible name. This element paints and says nothing; it is
aria-hiddento any assistive technology that reaches it via the markup below. Announcing the wait is the consumer's job, and which of the three patterns is right depends on what is waiting: - The wait replaces or fills a region that is already on the page — a table body, a panel, a list. Put
aria-busy="true"on that region and remove it when the content lands. The region already exists, so the state change is observable; nothing new needs announcing. <div id="results" aria-busy="true"> <span class="spinner" aria-hidden="true"></span> </div> - The wait is a standalone status the reader should hear about — "Saving…", "Checking availability". It needs a live region, and the live region has to exist in the DOM before it has content. A
role="status"inserted at the same moment as its text is frequently not announced at all, because there was no region for the platform to be watching. Render the empty container up front and write into it: <p class="spinner-status" role="status"> <span class="spinner spinner-sm" aria-hidden="true"></span> Checking availability… </p> A visible label is the better version whenever the design tolerates one: it tells sighted readers what is taking time, which the circle does not. Where it does not fit, wrap the same words in<span class="sr-only">— silence is not an option, the region needs text to announce. - The wait is a full-page block. Then the page is modal, and modality is
dialog.css's job — see below. Whichever you pick, do not also give the spinnerrole="status"itself: an empty live region that never receives text announces nothing, and now there are two things claiming to be the status.
src/css/components/spinner.css · npx mostlycss add spinner