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-6beside form fields,size-8for a section/page level wait (ReactclassName="size-8", Angularclass="size-8"). - The icon strokes
currentColor: it inherits the surrounding text color by default; settext-muted-foregroundfor a quiet page-level wait,text-primaryonly when the spinner is the emphasis of the surface.
Accessibility
- Ships
role="status"andaria-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-motionthe spin slows (2.5s per turn) instead of freezing — a static arc wouldn’t read as “in progress”. Deliberate exception to the library’smotion-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.