Spin

A loading indicator, on its own or wrapped around the content it is waiting for, with a delay so a fast response never flashes it.

Accessibility

Spin marks one element as busy and announces one line of text. The rest is up to you: naming the wait, saying when it is over, and keeping the keyboard out of the region it dims.

What it renders, and what it announces

Spin's root is a div with aria-live="polite" and aria-busy={spinning} in every form: bare, wrapping content, or fullscreen.
  • The dots have no role, label or text, so a bare spinner announces nothing on its own.
  • description is the only text Spin renders, so it is the only thing a screen reader can read out. Write one for any wait that matters.
  • The percent ring has role="progressbar" with its current value, but no name, so it announces a bare number. With percent="auto" that number is made up but announced as if it were real.
  • Spin takes no focus and has no keyboard behavior.

aria-busy and the region it covers

When Spin wraps children, aria-busy sits on the element around them. It tells assistive technology that the region is updating and what it reads now may be out of date.
  • Wrap only the region that is out of date. A Spin around the whole page says the navigation and header are updating too.
  • aria-busy is a hint. The content stays in the accessibility tree and can still be reached.
  • The new content lands inside the live region, so a large wrapped region can be read out all at once when the request finishes.

Announce the end, not just the start

A reader who cannot see the dots stop has no way to know the wait is over.
  • aria-busy turning false is not announced.
  • Say what arrived in the page's own words: a result count, a heading, an empty state. Text that replaces the description is read when it appears.
  • A failure needs this most. A spinner that disappears on error leaves the reader waiting. Render the error where the spinner was.
  • Do not add a second live region for the same event, or it is announced twice.

What wrapping does to the content behind it

While spinning, Spin dims the wrapped content, blocks the pointer and text selection on it, and lays an overlay on top. It looks blocked, and for a mouse it is.
  • The keyboard still gets in. Every button, link and field inside stays in the tab order, so a keyboard user can operate controls a mouse cannot reach.
  • If the wait must block input, disable the controls inside, or show the form in its own disabled state.
  • Focus stays where it was when the spinner appears, which may now be a dimmed control under an overlay.
  • fullscreen has the same gap: the mask blocks the pointer, but the page behind it keeps its tab order. It is not a dialog and does not trap focus.

Motion

The dots keep spinning when the reader asks for reduced motion.
  • Spin has no prefers-reduced-motion rule, and turning off the theme's motion token stops its fades but not the dots.
  • If your product honors reduced motion strictly, write that rule in your own stylesheet. Slow the dots down rather than freezing them: a spinner that does not move says the opposite of what it means.
  • Do not add more motion on top, like a pulse or a fade.

Contrast

The dots use colorPrimary, and each one fades to 0.3 opacity for part of its cycle, so contrast changes from frame to frame.
  • The spinner carries meaning, so it needs 3:1 against what is behind it. When it wraps content, check it against the dimmed content, not a clean background.
  • description is text and needs 4.5:1. Over wrapped content it has a text shadow to help, and a custom color can lose that margin.
  • On the fullscreen mask, which is dark in both themes, the spinner and its text turn light. A custom color that reads well inline can vanish there.
  • Never let the spinner be the only sign that something is happening. Pair it with words wherever the wait is long enough to need them.