Spinner

Loading indicator

Intent

Spinner is a pure-CSS rotating ring for loading states. Inline-block, sized in em by default so it tracks the surrounding font size, with explicit size variants for larger chrome. Use it for indeterminate waits -- fetching data, waiting for a response -- where there's nothing concrete to progress against.

Basic usage

A bare .spinner with role="status" and an aria-label announces itself as a loading state to assistive tech. The ring's visible arc paints in currentColor, so it inherits the surrounding text color unless a color variant says otherwise.

<span class="spinner" role="status" aria-label="Loading"></span>

Variants

Size

Sizes step through the density grammar: .is-sm (1em, slightly smaller than default for dense chrome), the default (1.25em, matches typical inline use), .is-lg (2rem, fixed-size for card or panel waits), .is-xl (3rem, for full-page or hero loading states). The default and .is-sm track the surrounding font-size; the two larger sizes are absolute so they don't grow with large ambient text.

<div class="cluster is-snug">
    <span class="spinner is-sm" role="status" aria-label="Loading"></span>
    <span class="spinner" role="status" aria-label="Loading"></span>
    <span class="spinner is-lg" role="status" aria-label="Loading"></span>
    <span class="spinner is-xl" role="status" aria-label="Loading"></span>
</div>

Color

The default spinner paints in currentColor so it inherits the surrounding text color -- useful when the spinner sits inside colored or themed chrome. .is-primary and .is-accent remap the arc color explicitly. The track (the faint ring behind the arc) is always the theme's shadow tone, not the variant color, so the arc reads as distinct from the ring.

<div class="cluster">
    <span class="spinner is-lg" role="status" aria-label="Loading"></span>
    <span class="spinner is-lg is-primary" role="status" aria-label="Loading"></span>
    <span class="spinner is-lg is-accent" role="status" aria-label="Loading"></span>
</div>

Inline with text

The default spinner is sized in em, so it scales with the element it sits inside -- a spinner inside <small> renders smaller than one inside a heading. Useful for inline loading phrases like "Saving…" where the spinner should match the text rhythm rather than impose its own scale.

Fetching your data

Loading dashboard

Saving draft
<div class="stack is-relaxed">
    <p class="cluster">Fetching your data <span class="spinner" aria-hidden="true"></span></p>
    <h3 class="cluster">Loading dashboard <span class="spinner" aria-hidden="true"></span></h3>
    <small class="cluster">Saving draft <span class="spinner" aria-hidden="true"></span></small>
</div>

Class reference

ClassRole
.spinner Inline-block rotating ring. Default size is 1.25em; default color is currentColor.
.is-sm, .is-lg, .is-xl Size variants. .is-sm tracks font-size; .is-lg and .is-xl are absolute.
.is-primary, .is-accent Color variants. Remap the arc color to the matching semantic family; track stays neutral.

Customization

Under prefers-reduced-motion: reduce the rotation slows to 1.5s rather than stopping, because users still need to know a wait is in progress. Full token list and defaults: src/css/spinner.css.

Accessibility

  • Announce the loading state. A visual spinner alone isn't announced by screen readers. Wrap with role="status" and an aria-label (or adjacent live-region text) so assistive tech announces "Loading" when the spinner appears. role="status" is a polite live region -- it waits for a pause in speech before announcing, which is the right tone for background waits.
  • Use aria-busy on the container that's loading. If the spinner sits inside a card that will eventually hold content, set aria-busy="true" on the card. Screen readers announce the busy state and suppress stale content; toggle to false when loading completes.
  • Decorative spinners get aria-hidden. When the surrounding copy already names the state ("Fetching your data…"), mark the spinner aria-hidden="true". The text is the announcement; the spinner is just the visual cue.
  • Button loading uses aria-busy, not .spinner. A button carrying aria-busy="true" renders its own spinner (shared keyframe) and gets the right semantics for free. Don't put .spinner inside a button's label.

CSS Reference: Spinner

CSS-only loading indicator

Source: src/css/spinner.css

Tokens

TokenDefaultDescription
--spinner-size1.25emDiameter of the spinner
--spinner-width2pxBorder thickness of the spinner ring
--spinner-colorcurrentColorColor of the spinning arc
--spinner-trackvar(--color-shadow)Color of the background track
--spinner-speed0.6sDuration of one full rotation

Classes

ClassDescription
.spinnercomponent root
.is-smSmaller spinner at 1em
.is-lgLarger spinner at 2rem with thicker ring
.is-xlExtra-large spinner at 3rem with thicker ring
.is-primaryPrimary color arc
.is-accentAccent color arc