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.
<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.
<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.
<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.
<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.
<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.
<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.
<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.
<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.