Spinner
Indeterminate loading spinner for async UI.
Status: stable. Group: feedback. Markdown version: /design-system/components/spinner.md
When to use
Spinner is the smallest loading primitive: a bare animated ring with role=status and an accessible label, sized to sit inline next to inputs, inside buttons, or centered in a small region. Use it for short waits of unknown duration (roughly a second or less). When completion is measurable or the wait is long, use Progress; when a layout is loading, mirror it with Skeleton. It renders no visible text; compose one alongside when sighted users also need the word.
When not to use
- Do not leave a spinner running past a couple of seconds without escalating; long waits need Progress or a Skeleton layout.
- Do not render more than one spinner per region; wrap the whole region in a single status instead.
- Do not use Spinner for waits you can measure or that routinely exceed a couple of seconds; that is Progress territory.
Look-alikes
- Center: Use it for single focal content: Spinner, EmptyState, a logo lockup.
- Progress: Do not use Spinner for waits you can measure or that routinely exceed a couple of seconds; that is Progress territory.
- Skeleton: Do not scatter one spinner per element while a view loads; use a Skeleton layout or one region-level spinner.
Props
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
label | string | none | Accessible name and announced text (required for standalone spinners). Defaults to the translated "Loading" (busyIndicator.loading). | No |
size | "sm" | "md" | "lg" | md | Size token: sm fits inside inputs/buttons, lg reads at region level. | No |
className | string | none | Optional utility classes on the root. | No |
Variants
- Available sizes:
sm,md,lg(defaultmd). Forwarded to CSS module size classes on Spinner.module.css.
Invalid combinations
The contract records no invalid combinations.
Keyboard
The contract records no keyboard interaction.
Rules
Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.
| Level | Rule | How it is checked |
|---|---|---|
| MUST | ARIA: Aria-label from the label prop, which defaults to the translated "Loading" (busyIndicator.loading); there is no aria-labelledby path. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Role=status that also holds the label as visually hidden text, so the live region has content to announce, not only a name. Announcement on insertion still depends on the screen reader; a status region that is already mounted when the label changes is announced most reliably. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by npm run test:stories:matrix:ci. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Name the wait in the label prop ("Validating password", not "Loading"); it is the accessible name AT users hear. | Not stated in the contract. |
| SHOULD | Match size to the host control: sm inside inputs and buttons, md standalone, lg for region-level waits. | Not stated in the contract. |
| SHOULD | Compose a visible Text label alongside for waits sighted users watch (see AppLoading); mark the visible copy aria-hidden so the announcement is single. The spinner still announces its own hidden label text. | Not stated in the contract. |
| SHOULD | Swap the spinner for a concrete outcome (success icon, error text) as soon as the wait resolves; do not leave it as the end state. | Not stated in the contract. |
| SHOULD NOT | Do not scatter one spinner per element while a view loads; use a Skeleton layout or one region-level spinner. | Not stated in the contract. |
Install and import
- Install:
npm install @digitaltableteur/react(version 0.9.1, restricted access on npm). - Import:
import { Spinner } from "@digitaltableteur/react"; - Entry point:
@digitaltableteur/react/feedbackexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-spinner>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.