Skip to main content

Spinner

A spinner shows that something is loading without saying how far along it is. It is the same ring Button and Combobox draw while busy, wrapped in a <span role="status"> with visually hidden text so assistive technology can read the busy state. It draws with currentColor, so any text colour utility colours it, and stops spinning under prefers-reduced-motion.

import Spinner from '@zuilib/components/spinner'

For the bare icon (no live region, aria-hidden), use @zuilib/components/lib/spinner instead.

Sizes

sm, md and lg follow the shared icon scale (size-4/5/6), so a spinner sits beside text at the same size as a chevron or check mark. xl (size-8) is for a page or panel level state. A consumer size-*, w-* or h-* on the root replaces the preset and the ring follows.

Loading example

Colour

The ring is currentColor. Set a text colour on the root or inherit one from the surrounding text.

Loading example

Announcing the busy state

Screen readers announce changes inside a live region that is already in the document, not a region that arrives together with its text. Keep the spinner mounted and toggle its label, or let the host carry the state (aria-busy on the panel, or a Button's loading, which announces itself). aria-label on the root is not live content and is never announced.

Loading example

Props

SpinnerProps extends the <span> attributes (minus children) with:

PropTypeDefaultDescription
size'sm' | 'md' | 'lg' | 'xl''md'size-4/5/6 on the icon scale; xl is size-8. Exposed as data-size
labelstring'Loading'Visually hidden text inside the live region. Pass '' when the host already names the state, leaving only the ring
classNamestringMerged last onto the root. A size-* / w-* / h-* here overrides the preset; a text colour colours the ring

Slots

SlotElementNotes
[data-slot="spinner"]spanRoot, role="status". Also data-size
[data-slot="spinner-icon"]svgThe ring, size-full, drawn in currentColor; aria-hidden
[data-slot="spinner-label"]spanThe visually hidden label; absent when label=""
/* Consumer CSS: brand-coloured spinners everywhere */
[data-slot="spinner"] {
color: var(--primary);
}

Tokens

TokenUsed for
--spacingThe size presets (size-4/5/6/8)
--duration-*Not used; the ring's rotation is a fixed keyframe that prefers-reduced-motion stops

Accessibility

  • The root is role="status" (an implicit polite live region) holding the hidden label; the SVG is aria-hidden.
  • Announcements happen on change: mount the spinner ahead of time and swap the label, or set aria-busy on the host instead.
  • Under prefers-reduced-motion the ring stops and stays as a static arc.
  • Nothing is focusable; the spinner is static.
  • Button: loading renders this ring inside the button and announces loadingText.
  • Progress: when you know how far along the work is.
  • Skeleton: a placeholder shaped like the content that is coming.