KKUONIDesign System

Components

Spinner

Beta

An 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.

Loading…Loading…Loading…

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

Loading…lg — 48 px
Loading…md — 24 px
Loading…sm — 16 px
  1. 1
    Track ringFull circle at 20% opacity of the current colour. Provides the visual context for the arc.
  2. 2
    Spinning arc90° arc in full colour that rotates at 0.75 s per revolution.
  3. 3
    sr-only labelVisually hidden text inside role="status" so screen readers announce the loading state.

Variants

Sizes

Loading…sm — 16 px
Loading…md — 24 px
Loading…lg — 48 px
SizeDiameterUse
sm16 pxInside button labels, compact inline contexts
md24 pxDefault — standalone panels, cards
lg48 pxFull-page loading state, hero-area transitions

Colour variants

Loading…teal
Loading…yellow
Loading…white
VariantUse
tealDefault — on white or light backgrounds
yellowOn teal backgrounds, or to match Wow Yellow accent
whiteOn dark or teal backgrounds (e.g. inside a loading overlay)

Custom label

Override the default "Loading…" screen-reader announcement for context-specific feedback.

Searching for available flights

Best practices

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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

PropTypeDefaultDescription
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.
labelstring'Loading…'Visually hidden text announced by screen readers via role="status". Customise to name the specific operation.
classNamestringAdditional 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
  • Progress Bar — use instead of Spinner when the operation's progress is measurable and can be communicated as a percentage.