Skip to content
Navigation

Spinner

Indeterminate loading indicator — a spinning lucide loader stroked in the current text color. For known progress use Progress instead.

Added 14 October 2026

Sizing and color

  • Default is size-4 (16px) — right for inline/button use. Scale with a size class: size-6 beside form fields, size-8 for a section/page level wait (React className="size-8", Angular class="size-8").
  • The icon strokes currentColor: it inherits the surrounding text color by default; set text-muted-foreground for a quiet page-level wait, text-primary only when the spinner is the emphasis of the surface.

Accessibility

  • Ships role="status" and aria-label="Loading" by default. Screen readers differ in whether they announce a live region that is inserted (rather than one whose text changes) — for a guaranteed announcement, keep a persistent status container and swap its text. [a11y]
  • Override the label when the context is more specific (“Saving campaign…”), and keep it in sync with the visible label if one is shown.
  • When the spinner sits beside a visible progressive label (“Sending…”), hide it from AT (aria-hidden="true") — otherwise its built-in “Loading” joins the accessible name (“Loading Sending…”). One live region per wait; don’t stack labels.
  • Under prefers-reduced-motion the spin slows (2.5s per turn) instead of freezing — a static arc wouldn’t read as “in progress”. Deliberate exception to the library’s motion-safe: discipline.

Busy buttons

While an async action runs: swap the label to a progressive form (“Sending…”, “Processing…”), lead with an aria-hidden spinner, and mark the button aria-busy="true" — see the Button doc’s busy state. The default size-4 matches the button icon slot; no size class needed. Prefer aria-disabled="true" over the native disabled for the just-clicked button — natively disabling it drops keyboard focus to the page body. [a11y]

Delayed appearance

For fast operations a spinner that flashes for 100ms reads as jank. Delay its appearance app-side (the ppv-self-booking DelayedSpinner pattern: center it and fade in after ~300ms, e.g. animate-in fade-in fill-mode-backwards delay-300). That wrapper is app-level composition, deliberately not part of the library.

Never

  • A spinner for determinate progress — use Progress.
  • Several spinners for one loading region — one indicator per wait; for content-shaped placeholders use Skeleton.
  • A bare spinner as the only feedback for a long operation with known steps — say what is happening.