Spinner
A hand-drawn indeterminate loading indicator that respects reduced motion.
Overview
A Spinner is an indeterminate busy indicator — a small hand-drawn ring that rotates while work of
unknown duration is in progress. Use it when you can’t show meaningful progress; use
Progress when you can.
Anatomy
A sketchy ellipse ring (@ghds/sketch-core) rendered in the primary colour and rotated. Because the
outline is hand-drawn and irregular, the rotation reads clearly without needing a gap or gradient.
Three sizes — sm, md (default), lg — map to comp.spinner.size.*; the spin duration is
comp.spinner.duration.
Variants & States
| State | Status | Notes |
|---|---|---|
| Spinning | Implemented | The only visual state — the ring rotates continuously. |
| Reduced motion | Implemented | Rotation is suppressed under prefers-reduced-motion; the ring is shown static. |
| Hover / Focus / Disabled | Not implemented | Spinner is not interactive. |
Usage
Do
- Give a meaningful label describing what is loading (e.g. "Loading results").
- Use for short, indeterminate waits; switch to Progress when you can report a percentage.
- Place the spinner where the incoming content will appear so focus/attention stays put.
Don't
- Don't use a spinner for waits long enough to warrant progress or a skeleton screen.
- Don't rely on motion alone — the accessible label conveys the busy state to assistive tech.
- Don't stack many spinners; one indicator per loading region is enough.
Accessibility
- React:
role="status"with anaria-label(default “Loading”), so the busy state is announced. The rotating SVG isaria-hidden. - Web Components:
gh-spinnersetsinternals.role = 'status'andinternals.ariaLabel. - React Native: sets
accessibilityLabelandaccessibilityState={{ busy: true }}(React Native has nostatusrole). - Reduced motion: all three platforms suppress the rotation when the user requests reduced motion
(
prefers-reduced-motionon web; the OS “reduce motion” setting on React Native). - See the Accessibility Guide.
Content
No visible text — the label is for assistive tech only. Keep it short and specific.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Rendered diameter. |
label | string | 'Loading' | Accessible label for the busy state. |
...rest | HTMLAttributes<HTMLSpanElement> | — | Native <span> attributes pass through. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Rendered diameter (reflected attribute). |
label | string | 'Loading' | Accessible label for the busy state. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Rendered diameter. |
label | string | 'Loading' | Accessible label for the busy state. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import { Spinner } from '@ghds/react/spinner';
<Spinner size="lg" label="Loading results" />;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/spinner';
</script>
<gh-spinner size="lg" label="Loading results"></gh-spinner>
// React Native — @ghds/react-native
import { Spinner } from '@ghds/react-native/spinner';
<Spinner size="lg" label="Loading results" />;