Feedbackstable.spinner
Spinner
A spinning ring that shows something is loading, with an accessible label.
Use it sparingly (UX guide §7.1): show nothing for the first ~300 ms, then a skeleton in the shape of the content for regions. A spinner fits small, shapeless waits — loading more rows, a refresh inside a card, an upload thumbnail.
Buttons have their own spinner: set busy on the button instead of placing a spinner next to it. Never cover the whole app with a spinner.
The label is visually hidden and announced via role="status". When the spinner sits next to visible text that already says what is happening ("Lädt Mitglieder …"), mark it decorative.
Examples
Default
Data
render({})Markup
<span class="spinner" role="status"><span class="u-visually-hidden">Wird geladen …</span></span>Large, specific label
Data
render({
"size": "lg",
"label": "Trainings werden geladen …"
})Markup
<span class="spinner" data-size="lg" role="status"><span class="u-visually-hidden">Trainings werden geladen …</span></span>Decorative next to text
Data
render({
"size": "sm",
"decorative": true
})Markup
<span class="spinner" data-size="sm" aria-hidden="true"></span>Data contract
What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | optional | "Wird geladen …" | What is loading, read by screen readers ("Trainings werden geladen …"). min 1 chars |
size | "sm" | "md" | "lg" | optional | "md" | Diameter, matching the icon sizes (design guide §9.4): sm 16 px (inline in small text), md 20 px, lg 24 px. |
decorative | boolean | optional | false | Hide from assistive technology because visible text next to it already says what is loading. |
JSON Schema
{
"type": "object",
"additionalProperties": false,
"required": [],
"properties": {
"label": {
"type": "string",
"minLength": 1,
"default": "Wird geladen …",
"description": "What is loading, read by screen readers (\"Trainings werden geladen …\")."
},
"size": {
"type": "string",
"enum": [
"sm",
"md",
"lg"
],
"default": "md",
"description": "Diameter, matching the icon sizes (design guide §9.4): `sm` 16 px (inline in small text), `md` 20 px, `lg` 24 px."
},
"decorative": {
"type": "boolean",
"default": false,
"description": "Hide from assistive technology because visible text next to it already says what is loading."
}
}
}Markup & states
Root: span.spinner — usable without render() by writing the markup directly.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
role="status" | — | — | Announces the hidden label. |
aria-hidden="true" | — | — | Decorative spinner. |
data-size | sm | lg | (none = md, --icon-md) | Diameter: --icon-sm / --icon-lg. |
Parts
| Part | Description |
|---|---|
(border) | 2 px ring in currentColor on a 25 % currentColor track — it takes the colour of the surrounding text. |
.u-visually-hidden | The label text. |
Accessibility
- The label is real text in a
role="status"region; decorative spinners arearia-hidden. - The spinner keeps rotating under reduced motion because it conveys state (design guide §8); the label communicates the state too.
- Remove the spinner (or replace its label with the result) when loading ends, so screen readers don't hear "loading" forever.
Guidance
Related: Button · Skeleton · Progress bar