Spinner

A lightweight indicator for in-flight work.

Border

The default. Use .spinner with role="status" and a visually hidden label so screen readers announce the loading state.

Loading…
<div class="spinner" role="status">
  <span class="visually-hidden">Loading…</span>
</div>

Grow

Add .spinner--grow for a pulsing dot. Same markup, same a11y story.

Loading…
<div class="spinner spinner--grow" role="status">
  <span class="visually-hidden">Loading…</span>
</div>

Colors

Both spinners inherit from currentColor, so any .text-* utility recolors them.

Loading…
Loading…
Loading…
Loading…
Loading…
Loading…
<div class="demo-row">
  <div class="spinner text-primary" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner text-success" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner text-danger" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner text-warning" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner text-info" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner text-muted-foreground" role="status"><span class="visually-hidden">Loading…</span></div>
</div>

The grow variant takes the same utilities.

Loading…
Loading…
Loading…
Loading…
Loading…
Loading…
<div class="demo-row">
  <div class="spinner spinner--grow text-primary" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow text-success" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow text-danger" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow text-warning" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow text-info" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow text-muted-foreground" role="status"><span class="visually-hidden">Loading…</span></div>
</div>

Sizes

Add .spinner--sm for the compact size used inside buttons and badges, or .spinner--lg for an empty-state hero.

Loading…
Loading…
Loading…
Loading…
Loading…
Loading…
<div class="demo-row">
  <div class="spinner spinner--sm" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--lg" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow spinner--sm" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow" role="status"><span class="visually-hidden">Loading…</span></div>
  <div class="spinner spinner--grow spinner--lg" role="status"><span class="visually-hidden">Loading…</span></div>
</div>

For one-off sizes, override --spinner-size inline.

Loading…
Loading…
<div class="demo-row">
  <div class="spinner" role="status" style="--spinner-size: 3rem; --spinner-width: 4px;">
    <span class="visually-hidden">Loading…</span>
  </div>
  <div class="spinner spinner--grow" role="status" style="--spinner-size: 3rem;">
    <span class="visually-hidden">Loading…</span>
  </div>
</div>

Alignment

Spinners are inline-block, so flex and margin utilities place them like any other inline element.

Syncing inbox
<div class="demo-row">
  <div class="spinner spinner--sm" role="status" aria-hidden="true"></div>
  <span>Syncing inbox</span>
</div>

Center one inside a card or empty state by wrapping it in a flex container.

Loading…
<div class="card">
  <div class="card__body d-flex justify-content-center" style="padding-block: 2.5rem;">
    <div class="spinner spinner--lg text-primary" role="status">
      <span class="visually-hidden">Loading…</span>
    </div>
  </div>
</div>

In buttons

For the canonical icon-aware loading state, use .is-loading on the button itself. It swaps any leading or trailing icon for the spinner without shifting the label. See Button.

<div class="demo-row">
  <button type="button" class="btn btn--primary is-loading" aria-busy="true">Saving</button>
  <button type="button" class="btn btn--outline btn--neutral is-loading" aria-busy="true">Loading</button>
</div>

For a standalone spinner inside a button (separate from .is-loading), slot .spinner.spinner--sm as a leading element.

<div class="demo-row">
  <button type="button" class="btn btn--primary" disabled>
    <span class="spinner spinner--sm" role="status" aria-hidden="true"></span>
    Loading
  </button>
  <button type="button" class="btn btn--primary" disabled>
    <span class="spinner spinner--grow spinner--sm" role="status" aria-hidden="true"></span>
    Loading
  </button>
</div>

Customization

Three variables retune .spinner without touching component CSS. Override on .spinner itself, on a parent scope, or on :root. The cascade scopes the change.

Variable Default Use
--spinner-size 1.25rem Diameter; sm and lg reassign this
--spinner-width 2px Stroke width on .spinner; ignored by .spinner--grow
--spinner-duration 0.75s One animation cycle; reduced-motion overrides to a longer value

Color comes from currentColor, so any .text-* utility recolors the spinner without a dedicated variable.