Data displaystable.skeleton
Skeleton
Grey placeholder rows in the shape of a list while its content loads.
Show a skeleton when loading takes longer than ~300 ms (show nothing before that to avoid flashes). It mimics the final layout — for lists: an optional avatar circle and one or two text lines per row — so the page doesn't jump when data arrives (UX guide §7.1).
Skeletons replace one region (a list, a card body), never the whole app. The container is marked aria-busy="true" and contains a visually hidden status text ("Lädt …"), so screen readers learn that content is coming.
Visuals (design guide §9.3): --color-surface-2 blocks, text lines at --text-md height with --radius-sm, avatars round (40 px, like the default avatar); a subtle opacity pulse based on --duration-slow, off under prefers-reduced-motion. Line widths are set with the --skeleton-width custom property (the only inline style allowed, §12.2).
Replace the whole skeleton with the content (or an empty state / error) in one step; remove aria-busy with it.
Examples
List with avatars
Data
render({
"rows": 3
})Markup
<div class="stack" aria-busy="true"><span class="u-visually-hidden" role="status">Lädt …</span><div class="cluster" aria-hidden="true"><span class="skeleton" data-shape="circle"></span><div class="stack u-grow" data-gap="sm"><span class="skeleton" style="--skeleton-width: 60%"></span><span class="skeleton" style="--skeleton-width: 35%"></span></div></div><div class="cluster" aria-hidden="true"><span class="skeleton" data-shape="circle"></span><div class="stack u-grow" data-gap="sm"><span class="skeleton" style="--skeleton-width: 50%"></span><span class="skeleton" style="--skeleton-width: 30%"></span></div></div><div class="cluster" aria-hidden="true"><span class="skeleton" data-shape="circle"></span><div class="stack u-grow" data-gap="sm"><span class="skeleton" style="--skeleton-width: 70%"></span><span class="skeleton" style="--skeleton-width: 40%"></span></div></div></div>Text-only rows
Data
render({
"rows": 4,
"avatar": false,
"lines": 1,
"label": "Trainings werden geladen …"
})Markup
<div class="stack" aria-busy="true"><span class="u-visually-hidden" role="status">Trainings werden geladen …</span><div class="cluster" aria-hidden="true"><div class="stack u-grow" data-gap="sm"><span class="skeleton" style="--skeleton-width: 60%"></span></div></div><div class="cluster" aria-hidden="true"><div class="stack u-grow" data-gap="sm"><span class="skeleton" style="--skeleton-width: 50%"></span></div></div><div class="cluster" aria-hidden="true"><div class="stack u-grow" data-gap="sm"><span class="skeleton" style="--skeleton-width: 70%"></span></div></div><div class="cluster" aria-hidden="true"><div class="stack u-grow" data-gap="sm"><span class="skeleton" style="--skeleton-width: 45%"></span></div></div></div>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 |
|---|---|---|---|---|
rows | integer | optional | 3 | Number of placeholder rows (match the expected count, max 10). ≥ 1 · ≤ 10 |
avatar | boolean | optional | true | Show a round avatar placeholder at the start of each row. |
lines | integer | optional | 2 | Text lines per row (title + meta = 2). ≥ 1 · ≤ 2 |
label | string | optional | "Lädt …" | Visually hidden status text announced to screen readers ("Mitglieder werden geladen …"). min 1 chars |
JSON Schema
{
"type": "object",
"additionalProperties": false,
"required": [],
"properties": {
"rows": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 3,
"description": "Number of placeholder rows (match the expected count, max 10)."
},
"avatar": {
"type": "boolean",
"default": true,
"description": "Show a round avatar placeholder at the start of each row."
},
"lines": {
"type": "integer",
"minimum": 1,
"maximum": 2,
"default": 2,
"description": "Text lines per row (title + meta = 2)."
},
"label": {
"type": "string",
"minLength": 1,
"default": "Lädt …",
"description": "Visually hidden status text announced to screen readers (\"Mitglieder werden geladen …\")."
}
}
}Markup & states
Root: span (inside div.stack[aria-busy]).skeleton — usable without render() by writing the markup directly.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
data-shape="circle" | — | — | Round 40 px placeholder (avatar). |
style="--skeleton-width: N%" | length / percentage | — | Width of a text line (default 100%). |
aria-busy="true" | — | — | On the container: region is loading. |
Parts
| Part | Description |
|---|---|
.stack[aria-busy] | Container replacing the region. |
.u-visually-hidden[role="status"] | Announced loading text. |
.cluster | One placeholder row (hidden from assistive tech). |
.skeleton | A --color-surface-2 block at --text-md height with a subtle opacity pulse. |
States
| State | Description |
|---|---|
@media (prefers-reduced-motion: reduce) | No pulse. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--skeleton-width | 100% | Width of a text-line placeholder. |
Accessibility
- Placeholder rows are
aria-hidden; only the status text is announced. - The container has
aria-busy="true"while loading. - The pulse is disabled for users who prefer reduced motion.
Guidance
Related: Empty state · List