Skip to main content

Spinner

Indicates that a process is loading or in progress. Use for small or inline loading states. Don't use for large content areas — use a skeleton loader pattern instead.

OverviewStyleAccessibility

Description

Anatomy

PartRequired?Notes
Track Required The full circular path that the indicator arc travels along. Rendered as a faint ring using --op-color-border-default at reduced opacity.
Arc indicator Required The animated segment that rotates around the track. Inherits colour from the surrounding context or an explicit variant token.
Label (visually hidden) Recommended An accessible text label passed via aria-label or a visually hidden element. Announced by screen readers in place of the animation, which conveys no meaning to assistive technology.

Variants

States

State Behaviour
Spinning The arc rotates continuously at a consistent speed. This is the only active state. The animation runs indefinitely until the process resolves or the component is removed from the DOM.
Reduced motion When the user has enabled prefers-reduced-motion, the continuous rotation is replaced by a pulsing opacity animation (fade in/out) so that motion-sensitive users still receive feedback without vestibular discomfort.
Hidden (resolved) Once the loading state ends, the spinner is removed or hidden and focus is managed to the resulting content or a relevant landmark, as appropriate.

Usage guidelines

Do / Don't

Do

Pair the spinner with a concise visible or screen-reader-only label that describes what is loading (e.g. 'Saving stakeholder record…').

Don't

Render the spinner alone with no accessible label. Screen readers will announce nothing meaningful about an unlabelled animation.

Do

Disable interactive controls (buttons, inputs) while the spinner is active to prevent duplicate submissions.

Don't

Leave the triggering button enabled while the spinner runs — users may click again, causing duplicate requests.

Do

Size the spinner to fit its container. Use the small size inside buttons and the default size in panels.

Don't

Place a large spinner inside a small button or tight inline context — it will break layout and look unpolished.

Do

Move focus to relevant content or show a success/error message once the spinner resolves.

Don't

Silently remove the spinner without any feedback. Users need confirmation that the action succeeded or failed.

Layout & Spacing

Spinner sizes map to the following dimensions:

  • Small (button use): 16x16px — use --op-space-16 as the outer bounding box
  • Default: 24x24px — use --op-space-24
  • Large (panel use): 32x32px — use --op-space-32

When displayed inline next to text, apply --op-space-8 (8px) of horizontal gap between the spinner and its label using a flex container with gap: var(--op-space-8).

When centred inside a loading container, use flexbox with align-items: center and justify-content: center. Do not rely on absolute positioning.

Tokens

PartTokenValue
Arc indicator (default) --op-color-text-primary Inherits text colour so the spinner fits naturally in any text context.
Arc indicator (primary variant) --op-color-interactive-default Green-400; matches the primary button fill to reinforce that an action is in progress.
Arc indicator (inverse variant) --op-color-text-on-interactive White; used on dark or filled backgrounds.
Arc indicator (muted variant) --op-color-text-tertiary Low-prominence; for background or auto-save operations.
Track --op-color-border-default Applied at 30% opacity to create a subtle guide ring without adding visual weight.
Animation duration 700ms One full rotation. Not a design token — hardcoded in the component. Reduced-motion variant uses a 1200ms opacity pulse instead.

Engineering notes

  • The component is implemented as a `wa-spinner` Web Awesome web component. Import via the shared Web Awesome bundle — no additional registration required.
  • Always supply an `aria-label` attribute on `wa-spinner` with a human-readable description of what is loading. Never rely on surrounding context alone — screen readers focus on the element, not its neighbours.
  • Set `aria-live="polite"` on the container region that will be updated once loading completes, not on the spinner itself. This ensures the resolved content is announced without interrupting the user mid-sentence.
  • When embedding inside a `wa-button`, add `loading` attribute to the button element rather than manually inserting a spinner — `wa-button` manages the spinner, disabled state, and ARIA internally.
  • To honour prefers-reduced-motion, the `wa-spinner` component applies a CSS media query internally. No additional implementation is needed unless building a custom spinner.
  • Remove the spinner from the DOM (or set `hidden`) once the process completes. Do not use visibility: hidden — this leaves the element in the accessibility tree.

Keyboard interaction

KeyAction
Tab The spinner itself is not focusable and does not appear in the tab order. Focus remains on the triggering control (e.g. the button) which should be disabled while loading.
Enter Not applicable — the spinner is a status indicator, not an interactive element.

Why it matters

Government platforms are required to meet WCAG 2.1 AA. Many users of council and agency portals rely on screen readers or have vestibular disorders that make continuous motion uncomfortable. A spinner without a text label is invisible to screen reader users, and an unthrottled animation can cause nausea for users with vestibular conditions. Both issues are avoidable with two lines of implementation.

Focus

The spinner does not receive focus. When a spinner appears as a result of a user action (e.g. clicking Submit), focus stays on the button. The button should be disabled and its label or aria-label updated to reflect the loading state (e.g. "Saving…"). When the action resolves, re-enable the button or move focus to the success message, updated region, or next logical element.

ARIA

Role or attributeWhen to useExample
aria-label Describes what is loading. Applied directly to the wa-spinner element. <wa-spinner aria-label="Saving stakeholder record"></wa-spinner>
aria-busy Applied to the container region that is loading. Set to true while the spinner is active, false when resolved. <div aria-busy="true" aria-live="polite">...</div>
aria-live Applied to the region that will receive the loaded content. Use polite for most cases; use assertive only for errors that require immediate attention. <section aria-live="polite" aria-atomic="true">...</section>
role="status" Can be applied to a visually hidden element that announces a dynamic status message (e.g. 'Loading complete') once the spinner resolves, without moving focus. <span role="status" class="sr-only">Consultation data loaded.</span>

Contrast

The arc indicator must meet a minimum 3:1 contrast ratio against its background (WCAG 1.4.11 non-text contrast).

  • Default (--op-color-text-primary on --op-color-bg-primary): passes at approximately 14:1 in light mode.
  • Primary (--op-color-interactive-default / green-400 on white): verify in the token reference — if the ratio falls below 3:1, switch to the primary-dark token variant for the arc.
  • Inverse (--op-color-text-on-interactive on --op-color-interactive-default): white on green-400 — confirm ratio meets 3:1.
  • Muted (--op-color-text-tertiary on --op-color-bg-primary): check that the tertiary text token still meets 3:1 for non-text elements. If not, use --op-color-text-secondary instead.

Touch targets

The spinner is not interactive and has no touch target requirement. If a spinner is placed inside a button or a tappable region, the 44x44px minimum touch target applies to the button, not the spinner itself.

Things to avoid

  • Avoid using `aria-hidden="true"` on a spinner that is the only loading feedback available — this makes the loading state completely invisible to screen reader users.
  • Avoid placing a spinner inside a `role="alert"` region — this will announce the loading state immediately and repeatedly as the animation re-renders, which is disruptive.
  • Avoid auto-focusing the spinner — it is not interactive and focus on a non-interactive element confuses keyboard users.
  • Avoid using colour alone to distinguish the spinner variant — ensure the label or surrounding context communicates the meaning.

Was this page helpful?

Updated 5 October 2026