Spinner

span.spinner

Spinner — the indeterminate busy indicator.

Examples

With something that announces it

View “With something that announces it” as

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.

Inside a region that is busy

View “Inside a region that is busy” as
<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.

Level 2 tokens declared by spinner
TokenDefault
--spinner-colorcurrentColor
--spinner-duration700ms
--spinner-size1.5rem
--spinner-thicknesscalc(var(--spinner-size) / 8)
--spinner-track-colorcolor-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

  1. A spinning circle is not an accessible name. This element paints and says nothing; it is aria-hidden to 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:
  2. 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>
  3. 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.
  4. 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 spinner role="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