A compact, indeterminate loading indicator for short waits. Four sizes, inherits the current text color or takes a tone, and exposes an accessible status label. Server-component safe.
import { Spinner } from "@/components/ui/spinner";
tsx
<Spinner label="Loading invoices" />
Buttons have their own spinner
For a button that is working, use Button's
isPending prop instead of placing a Spinner inside it. It shows a spinner,
blocks presses and announces the busy state for you.
Spinner — a short wait (roughly under a few seconds) with no measurable progress, in a small space: inline text, badges, list footers, a refreshing panel.
Skeleton — when you know the shape of the content that's loading; it avoids layout shift.
Progress Bar — when you can measure progress, or the wait is long enough that users need to know how far along it is.
Toast with toast.promise — for background work the user shouldn't wait on.
sm (16px) is the default and matches body text. xs (12px) fits badges and small buttons, md (20px) suits headings and toolbars, and lg (32px) is for panels and page-level loading.
label ("Loading" by default) becomes the accessible name. Make it specific when the spinner stands alone. When visible text next to it already says what's happening, pass aria-hidden so screen readers don't hear it twice.
Checking availability…
import { Spinner } from "@/components/ui/spinner";export default function SpinnerLabel() { return ( <div className="flex flex-col items-center gap-4 text-sm"> {/* Standalone: the label is the only description of what's happening. */} <Spinner size="md" label="Loading invoices" /> {/* Next to visible text: hide the spinner so the text isn't doubled. */} <p className="flex items-center gap-2 text-muted-foreground"> <Spinner aria-hidden /> Checking availability… </p> </div> );}
Cover stale content with a translucent layer and a centered spinner while it refreshes. Keep the old content visible underneath so the layout doesn't jump.
A step list where the running step shows a spinner and finished steps show a check. A visually hidden live region announces each step, since the spinners are hidden from screen readers.
Renders role="status" with aria-label set from label, so it's exposed as a named status element.
Give standalone spinners a specific label ("Loading invoices"). Next to visible text, pass aria-hidden instead.
A spinner that appears doesn't reliably announce itself. If completion matters, announce it with a live region or a toast, and set aria-busy on the region that is loading.
Respects prefers-reduced-motion: the ring stops rotating and fades gently in and out instead (motion-reduce:animate-pulse), so it still reads as "working" without movement. The role="status" label is announced either way. Add motion-reduce:animate-none to a spinner's className for a fully static ring.