Spinner

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

Wird geladen …
Data
render({})
Markup
<span class="spinner" role="status"><span class="u-visually-hidden">Wird geladen …</span></span>

Large, specific label

Trainings werden geladen …
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.

FieldTypeRequiredDefaultDescription
labelstringoptional"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.
decorativebooleanoptionalfalseHide 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

AttributeValuesDefaultDescription
role="status"——Announces the hidden label.
aria-hidden="true"——Decorative spinner.
data-sizesm | lg(none = md, --icon-md)Diameter: --icon-sm / --icon-lg.

Parts

PartDescription
(border)2 px ring in currentColor on a 25 % currentColor track — it takes the colour of the surrounding text.
.u-visually-hiddenThe label text.

Accessibility

  • The label is real text in a role="status" region; decorative spinners are aria-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

DoSay what is loading in the label.
DoPrefer skeletons for content regions.
Don'tDon't show a spinner for waits under ~300 ms.
Don'tDon't put a spinner next to a busy button — the button already shows one.

Related: Button · Skeleton · Progress bar