Skip to main content

Loading Status

Indicates that content or a process is in a loading or pending state. Use spinners for small inline states and skeleton variants for larger content areas. Don't use a spinner for a process with a known completion percentage.

OverviewStyleAccessibility

Description

Anatomy

PartRequired?Notes
Spinner track Required The full circular path the spinner animates along. Uses --op-color-border-default as the base track colour.
Spinner arc Required The animated portion that rotates around the track. Uses --op-color-interactive-default (green-400) for the active arc, or inherits the button's text colour when inside a button.
Label (spinner) Optional Short visible text beside the spinner, e.g. 'Loading...' or 'Saving...'. Omit when space is constrained; always provide an accessible label via aria-label regardless.
Skeleton block Required A grey rounded rectangle that stands in for a loading content region. Width and height should approximate the real content it replaces.
Skeleton shimmer Recommended A left-to-right animated gradient sweep across the skeleton block. Provides motion cue that the state is transient, not an error. Must be suppressed under prefers-reduced-motion.
Skeleton text lines Optional Multiple stacked skeleton blocks at text-line height, used to approximate a paragraph or list while it loads. Last line should be shorter (approx 60%) to mimic natural text wrap.

Variants

States

State Behaviour
Loading Spinner rotates continuously or skeleton shimmer animates. The surrounding container has aria-busy='true'. Interactive elements within the loading region are disabled.
Complete Loading indicator is removed and real content appears. aria-busy is removed or set to 'false'. No additional announcement is needed unless the content change is not visually apparent.
Error (post-load) Loading indicator is replaced by an error state. Use the system's inline error or empty state pattern. Do not leave a spinner running if loading has failed.
Reduced motion Spinner rotation and skeleton shimmer animation are suppressed via prefers-reduced-motion: reduce. The spinner track and arc remain visible as a static indicator; skeleton blocks remain visible without the sweep.

Usage guidelines

Do / Don't

Do

Replace skeleton placeholders with real content as soon as the data is available, even if other regions are still loading.

Don't

Hold all content until every request resolves — this maximises perceived wait time unnecessarily.

Do

Add a visible 'Loading stakeholders...' label or aria-label to spinners so screen reader users receive an equivalent experience.

Don't

Use a spinner with no accessible label — screen readers will announce nothing meaningful.

Do

Disable submit buttons and set aria-disabled while a form submission is in flight to prevent duplicate submissions.

Don't

Leave form controls active during loading — government users may submit duplicate consultation responses or stakeholder records.

Do

Size skeleton blocks to closely approximate the content they replace to minimise layout shift when content arrives.

Don't

Use a single full-width skeleton bar for every content type — mismatched skeletons increase perceived jank.

Layout & Spacing

Spinner sizes:

  • sm: 16px diameter — use inside buttons or inline with body text (--op-text-sm)
  • md: 24px diameter — default standalone spinner
  • lg: 40px diameter — overlay spinner centred in a panel

Spinner label gap: --op-space-8 (8px) between arc and label text

Skeleton blocks:

  • Border radius: --op-radius-sm (4px) for text lines, --op-radius-md (8px) for card blocks
  • Gap between stacked skeleton text lines: --op-space-8 (8px)
  • Card skeleton padding matches the card component's internal padding: --op-space-16 (16px)

Overlay spinner:

  • Container background: --op-color-bg-primary at 80% opacity
  • Spinner centred horizontally and vertically within the loading region
  • Min overlay height: 120px to avoid a spinner appearing clipped

Tokens

PartTokenValue
Spinner arc --op-color-interactive-default Green-400. Use --op-color-text-on-interactive when spinner appears inside a filled interactive button.
Spinner track --op-color-border-default Provides the grey base ring the arc animates over.
Skeleton block fill --op-color-bg-subtle Light neutral background for skeleton rectangles.
Skeleton shimmer highlight --op-color-bg-primary Used as the bright band in the shimmer gradient sweep.
Overlay background --op-color-bg-primary Applied at 80% opacity to the loading overlay container.
Spinner label text --op-color-text-secondary Subdued label keeps visual emphasis on the spinner itself.

Engineering notes

  • wa-spinner is the Web Awesome primitive for spinner variants. Set size via the size attribute (small, medium, large) or override with CSS custom properties.
  • Skeleton variants are not yet provided as wa-* primitives in this release. Implement as div elements with the op-skeleton utility class and the shimmer animation defined in the design token stylesheet.
  • Always set aria-label on wa-spinner when no visible label is present, e.g. aria-label='Loading stakeholders'.
  • When a spinner is placed inside a wa-button, set the button's loading attribute — this handles disabling the button, swapping to spinner display, and applying aria-busy automatically.
  • For overlay loading states, apply aria-busy='true' to the container element (not just the spinner), so assistive technologies announce the busy region correctly.
  • Suppress animations in the skeleton shimmer with: @media (prefers-reduced-motion: reduce) { .op-skeleton { animation: none; } }
  • When loading completes, move focus to the newly loaded content if the user initiated the load action (e.g. submitted a search). For passive background loads, do not move focus.

Keyboard interaction

KeyAction
Tab Focus skips over spinner and skeleton elements — they are not interactive. Focus management is handled by the triggering control (e.g. button).
Enter Not applicable to loading indicators directly. The triggering button should be disabled (aria-disabled='true') while loading is in flight.

Why it matters

Government users include staff and members of the public who rely on screen readers or keyboard navigation. A spinner with no label leaves these users with no indication that the system is working, which in a consultation or engagement context can lead to repeat submissions and corrupted data.

Focus

Do not place focus on a spinner or skeleton element. When a load is triggered by a user action:

  • Disable the triggering control and set aria-disabled='true' while loading.
  • On load completion, return focus to the triggering control or move it to the top of the newly loaded content region, depending on whether the loaded content replaces the current view.
  • For background/passive loads (e.g. a table refreshing on an interval), do not move focus.

ARIA

Role or attributeWhen to useExample
aria-busy Apply to the container region that is loading, not to the spinner itself. <div aria-busy="true" aria-label="Loading stakeholder list">...</div>
aria-label Apply to wa-spinner when no visible text label accompanies it. <wa-spinner aria-label="Saving consultation"></wa-spinner>
aria-live Use aria-live='polite' on a visually hidden status region to announce completion or error after loading, if the content change is not self-evident from focus movement. <p aria-live="polite" class="sr-only">Stakeholders loaded.</p>
role="status" Can be applied to a wrapper element to implicitly create an aria-live='polite' region for load completion announcements. <div role="status" class="sr-only">Consultation saved successfully.</div>

Contrast

Spinner arc (--op-color-interactive-default, green-400) against the spinner track (--op-color-border-default on --op-color-bg-primary background) meets WCAG 2.1 AA non-text contrast of 3:1 minimum. Spinner label text uses --op-color-text-secondary, which meets 4.5:1 contrast against --op-color-bg-primary. Skeleton blocks use --op-color-bg-subtle, which is a decorative placeholder — contrast requirements for non-text UI components apply (3:1), and the shimmer gradient must not be the sole indicator of loading state.

Touch targets

Spinners are non-interactive and do not require a 44x44px touch target. However, any button containing a spinner must retain its full touch target size (minimum 44x44px) and must not shrink when switching to a loading state.

Things to avoid

  • Do not use colour alone to distinguish a loading state from a static state — the spinner animation (or visible skeleton structure) must be the primary indicator.
  • Do not leave a spinner running indefinitely — implement a timeout and surface an error state if loading exceeds a reasonable threshold (recommended: 30 seconds for government data queries).
  • Do not animate the skeleton shimmer for users who have prefers-reduced-motion enabled.
  • Do not use a spinner inside a disabled button without also setting aria-disabled='true' — sighted users see the spinner, but assistive technologies need the attribute.

Was this page helpful?

Updated 5 October 2026