A spinning loading indicator. Zero JS.

Last updated

Show code
<Spinner label="Loading" />

Add

npx @ava-ui/apogee add spinner

Ships @lucide/astro (LoaderCircle). No Apogee component dependencies.

Usage

---
import Spinner from '@/components/apogee/Spinner/Spinner.astro'
---
<Spinner label="Loading" />

API

@composable — exposed to Content Editors in AVA. Any other SVG attribute passes through (e.g. data-*).

Prop Type Default Description
size 'small' | 'medium' | 'large' 'medium' Icon size (size-4 / size-5 / size-8).
label string — Accessible label. Set it for a standalone spinner (renders role="status" + aria-label); omit it when a parent already conveys loading (e.g. Button’s aria-busy) — it then renders aria-hidden.
color 'inherit' | 'primary' | 'neutral' | 'critical' | 'positive' | 'warning' 'inherit' Hue; inherit takes the surrounding text colour.
class string — Extra classes, merged last.

Sizes

Every size:

Show code
<Spinner size="small" label="Loading" />
<Spinner label="Loading" />
<Spinner size="large" label="Loading" />

Colours

Show code
<Spinner color="inherit" label="inherit" />
<Spinner color="primary" label="primary" />
<Spinner color="neutral" label="neutral" />
<Spinner color="critical" label="critical" />
<Spinner color="positive" label="positive" />
<Spinner color="warning" label="warning" />

States

Always animating (animate-spin) — no hover/focus/disabled states; it’s not interactive. Inherits text-current, so it takes the colour of its container:

Show code
<div class="text-primary"><Spinner label="Loading" /></div>
<div class="text-critical"><Spinner label="Loading" /></div>

Examples

Standalone (announced) vs. decorative (parent already labelled):

<!-- Standalone: announced to screen readers -->
<Spinner label="Loading results" />

<!-- Inside an already-labelled context, e.g. a loading Button -->
<Spinner />

Accessibility

  • With label set: renders role="status" + aria-label={label} — announced to assistive tech. It announces on mount, not on later updates (no aria-live); if the spinner appears without a full page navigation, consider wrapping it in your own aria-live region.
  • Without label: renders aria-hidden="true" — purely decorative, for use inside a parent that already communicates loading (e.g. Button’s loading prop sets aria-busy on the button itself).
  • The spin animation (animate-spin) is paired with motion-reduce:[animation-duration:1.6s] — it slows rather than stops under prefers-reduced-motion, since the spin itself is the loading indicator.
  • No JS: this component ships zero JavaScript.

Tokens

Spinner reads no Apogee tokens directly — it inherits colour via currentColor. Set a text-colour utility (text-primary, text-critical, …) on a wrapping element, or pass class, to theme it.

Do / Don’t

  • Do set label whenever the spinner stands alone (a full-page loader, a panel’s loading state) — it’s the only thing announcing “loading” to screen reader users.
  • Do let it inherit colour rather than hard-coding one, so it matches its surrounding context.
  • Don’t set label on a Spinner that’s already inside a labelled loading control (e.g. Button loading) — that double-announces “loading.”
  • Don’t use Spinner for a placeholder-shaped loading state (a card, a list row) — use Skeleton.

Content

label is a short present-participle phrase (“Loading”, “Saving changes”, “Loading results”) — not a full sentence, no ending punctuation.