Spinner
An indeterminate loading indicator — a spinning icon that inherits currentColor, in four sizes, accessible by default.
- Status
- Since
0.1.0- Accessibility pattern
- ARIA status region
Last updated
Install
Add Spinner from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/spinnerUsage
import { Spinner } from "@/components/ui/spinner";
<Spinner />;The spinner inherits currentColor, so set its color via the surrounding text color:
<span className="text-muted-foreground">
<Spinner />
</span>Examples
With a label
The recommended pattern: pair the spinner with visible loading text. The text labels the
loading region, so the spinner itself is marked decorative to avoid a double announcement
to assistive tech.
<span className="flex items-center gap-2 text-muted-foreground">
<Spinner size="sm" decorative />
Saving changes…
</span>Color
The spinner has no hardcoded color — it draws in currentColor and defaults to
text-muted-foreground. Recolor it by setting the text color on any ancestor (or via a
className), including inside a button where it inherits the button's text color.
<span className="text-primary">
<Spinner />
</span>;
<Button disabled>
<Spinner size="sm" label="" />
Saving…
</Button>;Sizes
xs, sm, md, and lg — mapped to the size-* token scale.
Playground
Step through the four sizes, then copy the generated JSX.
<Spinner />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
decorative | boolean | false | Marks the spinner as decoration: aria-hidden, no role, no label. Use it when the
surrounding UI already announces the loading state — a button with loading text, or a
sibling live region that says "Saving…" — so the announcement is not made twice.
This is the sanctioned way to say it (audit B8-09). label="" also works and means the
same thing, but it says it by passing a value that reads as a mistake at the call site. |
label | string | 'Loading' | Accessible label announced by assistive tech while the spinner is visible. The spinner
exposes role="status" + aria-label so screen readers announce the loading state. |
size | "inherit" | "lg" | "md" | "sm" | "xs" | 'md' | Size variant — mirrors the rest of the scale and maps to the size-*
tokens. The spinner inherits currentColor, so set its color via the
parent's text color. |
Data attributes and CSS variables on Spinner
| Attribute | Values |
|---|---|
data-size | mirrors a prop or state value |
data-slot | "spinner" |
Accessibility
- By default the spinner renders
role="status"witharia-label="Loading", so assistive tech announces the loading state. Override the wording with thelabelprop (e.g."Saving changes"). - When the surrounding UI already labels the loading region — for example a button that swaps its text
to "Saving…" — pass
decorativeto hide the spinner from assistive tech (aria-hidden) and avoid a double announcement. That is the sanctioned spelling:label=""means the same thing but says it by passing a value that reads as a mistake at the call site. - Motion respects user preference: the spin animation is disabled under
prefers-reduced-motion: reduce(motion-reduce:animate-none). - Color is conveyed through
currentColoronly — never rely on the spinner alone to communicate meaning; pair it with text where the state matters.
| Concern | Behavior |
|---|---|
| Role | role="status" (live region) unless decorative |
| Accessible name | aria-label from the label prop (default "Loading") |
| Reduced motion | Animation removed under prefers-reduced-motion: reduce |
| Contract | States tested |
|---|---|
| Behaviour | default, empty, indeterminate, loading |
| Accessibility | labeled, status-announcement |
| Visual | default, loading, empty |