Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Spinner

An indeterminate loading indicator — a spinning icon that inherits currentColor, in four sizes, accessible by default.

Status
stable
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/spinner

Usage

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>
Saving changes…

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

PropTypeDefaultDescription
decorativebooleanfalseMarks 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.
labelstring'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

AttributeValues
data-sizemirrors a prop or state value
data-slot"spinner"

Accessibility

  • By default the spinner renders role="status" with aria-label="Loading", so assistive tech announces the loading state. Override the wording with the label prop (e.g. "Saving changes").
  • When the surrounding UI already labels the loading region — for example a button that swaps its text to "Saving…" — pass decorative to 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 currentColor only — never rely on the spinner alone to communicate meaning; pair it with text where the state matters.
ConcernBehavior
Rolerole="status" (live region) unless decorative
Accessible namearia-label from the label prop (default "Loading")
Reduced motionAnimation removed under prefers-reduced-motion: reduce
ContractStates tested
Behaviourdefault, empty, indeterminate, loading
Accessibilitylabeled, status-announcement
Visualdefault, loading, empty

Do / Don't

Do
Give the spinner a meaningful label (Saving changes…), or pair it with visible loading text and mark it decorative with label=''.
Don't
Render two announced spinners in the same region, or rely on the spinner alone — with no text — to explain what is loading.

On this page