Spinner

User Interface (UI)

How a spinner looks, from its parts, size and colour to how it moves and the one variable you can set.

Let's start with how a spinner looks: its parts, its size and colour, how it moves and the one variable you can set.

Anatomy

  1. Container — The <svg>, a square that turns as a whole, without taking up any more room.
  2. Track — A faint full circle, which shows the path the indicator follows.
  3. Indicator — A quarter of the circle at full strength, which turns around the track.

Every spinner has all three parts, and nothing else. The indicator is what people watch, so it's the part that has to stand out (see Contrast), while the track stays faint. We recommend drawing both as strokes in one <svg>, with a 16 × 16 viewBox and a track at 25% opacity. The parts always render in the same order, which you can see in the DOM contract.

Size and colour

The Spinner takes its size and colour from where it sits, just like an icon. It's 1em wide, the size of the text around it, and it's drawn in currentColor, the colour of that text. Next to a label, it lines up without any setup, and to give it another size, you can set --spinner--size from a class.

Saving draft

Loading results

Loading.tsx
import styles from "./Loading.module.css";

export function Loading() {
  return (
    <>
      <p>
        <Spinner /> Saving draft
      </p>
      <h2>
        <Spinner /> Loading results
      </h2>
      <Spinner className={styles.large} />
    </>
  );
}
Loading.module.css
.large {
  --spinner--size: 2rem;
}

Inside a component, the component sets the size. In a Button, the Spinner is as big as the Button's icons, with --button--icon--size. A spinner on its own, in the middle of a panel that's loading, is where a larger size comes in.

Keep in mind that these examples only show sizes. On a real page, the text that says what's loading goes in a status message, so screen readers hear it too, as under Say what's loading.

States

The Spinner can't be hovered, focused or pressed, so it has no interactive states. It's in the page while something's busy and gone once it's done, and while it's there, it moves in one of two ways:

StateSelectorRecommended look
Turning.spinnerOne full turn every 0.8 seconds, at a steady speed
Reduced motion@media (prefers-reduced-motion: reduce)No turning. The whole spinner fades to 40% and back every 1.6 seconds

You'll find why it pulses under Motion. The wait it shows has states of its own, like done and failed, which belong to the page, and you'll find those under Every state.

Tokens

The Spinner doesn't read any theme tokens. Its colour is currentColor, so it follows whatever colour, theme or colour scheme the text around it follows. It has a single variable of its own:

VariableDefaultSet by
--spinner--size1emComponents that place it, like the Button with --button--icon--size, and your classes

The Spinner's styles sit in the components cascade layer, so a class of yours that sets --spinner--size wins without any specificity tricks, as in the example above. To change its colour, set color on it, or on the text around it.

UI Checklist

RuleLevelScopeRequirementBasisCheck
spinner/em-sizedShouldComponentIt's 1em by default, so it matches the text around it, and --spinner--size sets any other sizeOpen ComponentsReview
spinner/current-colorMustComponentIt's drawn in currentColor, so it follows the text around it, every colour scheme and forced colours modeOpen ComponentsEmulation
spinner/contrastMustBothThe indicator has a contrast of 3:1 against what's behind itWCAG 1.4.11 (AA)Contrast checker
spinner/reduced-motionMustComponentIt doesn't turn when prefers-reduced-motion: reduce is set, but still shows that something's happening, like with a pulseWCAG 2.3.3 (AAA), Open ComponentsEmulation
spinner/stable-layoutMustBothIt keeps a fixed size while it turns, and showing or hiding it doesn't move anything around itOpen ComponentsVisual regression test
Copyright © 2026