Components
Spinner
BetaAn animated circular loading indicator for indeterminate wait states. Used in button loading states, page transitions, and data-fetching flows where no content shape exists to mirror.
Purpose
The Spinner communicates that an operation is in progress and the user must wait. It is the right choice when:
- No content shape exists — the page or section is completely empty while loading. Use Skeleton when the shape of the arriving content is predictable.
- The wait is indeterminate — you cannot estimate how long the operation will take.
- The loading is triggered by an action — a button press, form submit, or navigation event.
Common uses:
- Button loading state — next to the label while an API call completes
- Page transition — full-page Spinner while the next route loads
- Inline data fetch — inside a card or panel while remote data arrives
The Spinner and the Skeleton serve different purposes. Use Spinner for actions; use Skeleton for initial page loads where the layout shape is known.
Usage
Reach for Spinner when there is no predictable content shape to placeholder and the wait is indeterminate — an action in progress, a page transition, or a data fetch with no known layout. When the shape of the arriving content is known in advance (a grid of cards, a table of rows), use a Skeleton loader instead so the layout doesn't jump once content resolves. For a determinate, measurable process (e.g. a multi-step file upload), use Progress Bar instead, since it can communicate how far along the operation is.
Spinner most commonly appears inside a Button's loading state, as a full-page loader during route transitions, and inline within a card or panel while remote data is fetched.
Anatomy
- 1Track ring — Full circle at 20% opacity of the current colour. Provides the visual context for the arc.
- 2Spinning arc — 90° arc in full colour that rotates at 0.75 s per revolution.
- 3sr-only label — Visually hidden text inside role="status" so screen readers announce the loading state.
Variants
Sizes
| Size | Diameter | Use |
|---|---|---|
sm | 16 px | Inside button labels, compact inline contexts |
md | 24 px | Default — standalone panels, cards |
lg | 48 px | Full-page loading state, hero-area transitions |
Colour variants
| Variant | Use |
|---|---|
teal | Default — on white or light backgrounds |
yellow | On teal backgrounds, or to match Wow Yellow accent |
white | On dark or teal backgrounds (e.g. inside a loading overlay) |
Custom label
Override the default "Loading…" screen-reader announcement for context-specific feedback.
Best practices
Use one Spinner per loading region. If a whole page is loading, one Spinner in the page centre is enough.
A single Spinner in the centre of a loading panel keeps the layout clean and the message clear.
Place multiple Spinners on the same loading state unless genuinely different regions load separately.
Multiple Spinners at different sizes create visual noise and suggest that different regions are loading independently when they are not.
Pair the Spinner with visible or hidden text that names the operation.
'Checking availability…' in the Button label alongside a sm Spinner tells the user exactly what is happening.
Use a standalone Spinner without any surrounding context or label.
A Spinner alone with no surrounding context leaves users wondering whether the page is broken or loading.
Use Skeleton when the arriving content has a predictable shape (cards, tables, paragraphs).
Use Skeleton instead when a destination card grid is loading — the card shapes tell users what is coming.
Substitute Spinner for Skeleton when content shape is known and the layout would benefit from placeholder shapes.
A Spinner in a card grid removes the shape signal — users lose spatial context for what is loading.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| size | 'sm' | 'md' | 'lg' | 'md' | Diameter of the spinner. sm=16px, md=24px, lg=48px. |
| color | 'teal' | 'yellow' | 'white' | 'teal' | Colour of the spinning arc and track. Use white on dark backgrounds. |
| label | string | 'Loading…' | Visually hidden text announced by screen readers via role="status". Customise to name the specific operation. |
| className | string | — | Additional Tailwind classes on the root span. |
Accessibility
Screen reader announcement
The Spinner renders a role="status" container with a visually hidden label. Screen readers announce it when it appears in the DOM:
<span role="status" aria-label="Loading…">
<svg aria-hidden="true" className="animate-spin …">…</svg>
<span className="sr-only">Loading…</span>
</span>
role="status" is an ARIA live region with aria-live="polite" — it announces when the spinner appears without interrupting the user's current focus.
Naming specific operations
Always override label to describe what is loading:
{/* ✅ Context-specific */}
<Spinner label="Searching for available holidays" />
{/* ❌ Generic — gives users no information about the operation */}
<Spinner />
Button loading pattern
When a Spinner replaces a Button's icon while loading, the button should keep its text label visible. See the Button component for the full loading state pattern:
<Button loading loadingLabel="Submitting your booking">
Book now
</Button>
The loadingLabel on Button injects the same sr-only announcement used by Spinner.
Keyboard interaction
The Spinner itself is not interactive and receives no keyboard focus. Ensure the surrounding context handles focus correctly:
- Disable the triggering button while loading so users cannot re-submit
- Return focus to a relevant element when loading completes
Related components
- Progress Bar — use instead of Spinner when the operation's progress is measurable and can be communicated as a percentage.