Data displaystable.empty
Empty state
Explains why a list or area is empty and offers the next step.
Every list, table and dashboard designs its empty state (UX guide §7.2). Three kinds: first use ("Noch keine Lektionspläne." + "Plan erstellen"), no results ("Keine Mitglieder für «Mueler»." + "Filter zurücksetzen") and all done ("Keine offenen Anmeldungen. Gut gemacht.", usually without action).
Anatomy (design guide §9.3): a 24 px icon in a 48 px --color-surface-2 circle, a short title saying what is (not) there, one sentence explaining why or what to do, and at most one action (Button data, often a link via href). Centred in its container, max --readable-max wide.
Set live: true when the empty state appears as the result of the user's own search or filter, so screen readers hear "Keine Mitglieder gefunden" without moving focus.
Examples
Nothing today (with link)
Data
render({
"icon": "calendar",
"title": "Heute keine Trainings",
"text": "Du bist für heute für kein Training eingetragen.",
"action": {
"label": "Ganze Woche ansehen",
"href": "#woche"
}
})Markup
<div class="empty"><span class="empty__icon"><svg class="icon" data-size="lg" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#calendar"/></svg></span><p class="empty__title">Heute keine Trainings</p><p>Du bist für heute für kein Training eingetragen.</p><a class="button" href="#woche">Ganze Woche ansehen</a></div>First use
Noch keine Lektionspläne
Erstelle deinen ersten Plan oder übernimm einen Vorschlag.
Data
render({
"icon": "list",
"title": "Noch keine Lektionspläne",
"text": "Erstelle deinen ersten Plan oder übernimm einen Vorschlag.",
"action": {
"label": "Plan erstellen",
"variant": "primary",
"icon": "plus"
}
})Markup
<div class="empty"><span class="empty__icon"><svg class="icon" data-size="lg" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#list"/></svg></span><p class="empty__title">Noch keine Lektionspläne</p><p>Erstelle deinen ersten Plan oder übernimm einen Vorschlag.</p><button class="button" type="button" data-variant="primary"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#plus"/></svg>Plan erstellen</button></div>No results
Keine Mitglieder für «Mueler»
Prüfe die Schreibweise oder entferne Filter.
Data
render({
"icon": "search",
"title": "Keine Mitglieder für «Mueler»",
"text": "Prüfe die Schreibweise oder entferne Filter.",
"action": {
"label": "Filter zurücksetzen"
},
"live": true
})Markup
<div class="empty" role="status"><span class="empty__icon"><svg class="icon" data-size="lg" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#search"/></svg></span><p class="empty__title">Keine Mitglieder für «Mueler»</p><p>Prüfe die Schreibweise oder entferne Filter.</p><button class="button" type="button">Filter zurücksetzen</button></div>All done
Keine offenen Anmeldungen
Gut gemacht. Neue Anmeldungen erscheinen hier.
Data
render({
"icon": "check-circle",
"title": "Keine offenen Anmeldungen",
"text": "Gut gemacht. Neue Anmeldungen erscheinen hier."
})Markup
<div class="empty"><span class="empty__icon"><svg class="icon" data-size="lg" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check-circle"/></svg></span><p class="empty__title">Keine offenen Anmeldungen</p><p>Gut gemacht. Neue Anmeldungen erscheinen hier.</p></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 |
|---|---|---|---|---|
icon | string | optional | "info" | Icon id from the sprite that fits the content ("calendar", "users", "search", "check-circle"). |
title | string | required | — | What is (not) there: "Heute keine Trainings". min 1 chars |
headingLevel | integer | optional | — | Render the title as h2–h4 when the empty state replaces a whole section; otherwise it is a p.≥ 2 · ≤ 4 |
text | string | optional | — | One sentence: why it is empty or what to do next. |
action | object | optional | — | At most one action (Button data), e.g. { label: "Plan erstellen", variant: "primary" } or a link with href. |
action.label | string | optional | — | Visible text. Required unless iconOnly.min 1 chars |
action.variant | "secondary" | "primary" | "ghost" | "danger" | "danger-ghost" | optional | "secondary" | Visual weight / meaning. |
action.size | "sm" | "md" | "lg" | optional | "md" | sm (36 px) only in dense desktop tables; lg (52 px) for the single main action of a screen. |
action.icon | string | optional | — | Icon id from the sprite, shown before the label. |
action.iconOnly | boolean | optional | false | Square button showing only icon. Requires ariaLabel. |
action.ariaLabel | string | optional | — | Accessible name when the visible label is missing or ambiguous ("Weitere Aktionen für Lea Müller"). |
action.href | string | optional | — | Render an <a> that looks like a button (navigation, not actions). |
action.type | "button" | "submit" | "reset" | optional | "button" | Button type inside forms. |
action.value | string | optional | — | Form value, e.g. cancel / confirm inside <form method="dialog">. |
action.name | string | optional | — | Form field name when the button submits a value. |
action.disabled | boolean | optional | false | Not available now. Prefer explaining why nearby over silently disabling. |
action.busy | boolean | optional | false | Action in progress (aria-busy). |
action.formNoValidate | boolean | optional | false | Submit without native validation — use on "Abbrechen" inside <form method="dialog"> so required fields do not block closing. |
action.block | "always" | "mobile" | optional | — | Full width always, or only below 768 px. |
action.behavior | string | optional | — | Behaviour hook (data-behavior), e.g. open-dialog, theme-toggle. |
action.action | string | optional | — | Screen action name (data-action). Declared in the actions of the screen with a handler and a risk class; the engine adds undo or confirm accordingly.pattern ^[a-z][a-zA-Z0-9-]*$ |
action.actionId | string | optional | — | Id of the thing the action applies to (data-id), when the button is not inside an element that has one.pattern ^[A-Za-z0-9_-]+$ |
action.target | string | optional | — | Id of the element a behaviour acts on (data-target), e.g. the dialog to open. |
action.popoverTarget | string | optional | — | Id of a popover menu this button toggles (popovertarget). |
live | boolean | optional | false | Announce politely (role="status") — for empty search/filter results. |
JSON Schema
{
"type": "object",
"additionalProperties": false,
"required": [
"title"
],
"properties": {
"icon": {
"type": "string",
"default": "info",
"description": "Icon id from the sprite that fits the content (\"calendar\", \"users\", \"search\", \"check-circle\")."
},
"title": {
"type": "string",
"minLength": 1,
"description": "What is (not) there: \"Heute keine Trainings\"."
},
"headingLevel": {
"type": "integer",
"minimum": 2,
"maximum": 4,
"description": "Render the title as `h2`–`h4` when the empty state replaces a whole section; otherwise it is a `p`."
},
"text": {
"type": "string",
"description": "One sentence: why it is empty or what to do next."
},
"action": {
"type": "object",
"additionalProperties": false,
"required": [],
"properties": {
"label": {
"type": "string",
"minLength": 1,
"description": "Visible text. Required unless `iconOnly`."
},
"variant": {
"type": "string",
"enum": [
"secondary",
"primary",
"ghost",
"danger",
"danger-ghost"
],
"default": "secondary",
"description": "Visual weight / meaning."
},
"size": {
"type": "string",
"enum": [
"sm",
"md",
"lg"
],
"default": "md",
"description": "`sm` (36 px) only in dense desktop tables; `lg` (52 px) for the single main action of a screen."
},
"icon": {
"type": "string",
"description": "Icon id from the sprite, shown before the label."
},
"iconOnly": {
"type": "boolean",
"default": false,
"description": "Square button showing only `icon`. Requires `ariaLabel`."
},
"ariaLabel": {
"type": "string",
"description": "Accessible name when the visible label is missing or ambiguous (\"Weitere Aktionen für Lea Müller\")."
},
"href": {
"type": "string",
"description": "Render an `<a>` that looks like a button (navigation, not actions)."
},
"type": {
"type": "string",
"enum": [
"button",
"submit",
"reset"
],
"default": "button",
"description": "Button type inside forms."
},
"value": {
"type": "string",
"description": "Form value, e.g. `cancel` / `confirm` inside `<form method=\"dialog\">`."
},
"name": {
"type": "string",
"description": "Form field name when the button submits a value."
},
"disabled": {
"type": "boolean",
"default": false,
"description": "Not available now. Prefer explaining why nearby over silently disabling."
},
"busy": {
"type": "boolean",
"default": false,
"description": "Action in progress (`aria-busy`)."
},
"formNoValidate": {
"type": "boolean",
"default": false,
"description": "Submit without native validation — use on \"Abbrechen\" inside `<form method=\"dialog\">` so required fields do not block closing."
},
"block": {
"type": "string",
"enum": [
"always",
"mobile"
],
"description": "Full width always, or only below 768 px."
},
"behavior": {
"type": "string",
"description": "Behaviour hook (`data-behavior`), e.g. `open-dialog`, `theme-toggle`."
},
"action": {
"type": "string",
"pattern": "^[a-z][a-zA-Z0-9-]*$",
"description": "Screen action name (`data-action`). Declared in the `actions` of the screen with a handler and a risk class; the engine adds undo or confirm accordingly."
},
"actionId": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]+$",
"description": "Id of the thing the action applies to (`data-id`), when the button is not inside an element that has one."
},
"target": {
"type": "string",
"description": "Id of the element a behaviour acts on (`data-target`), e.g. the dialog to open."
},
"popoverTarget": {
"type": "string",
"description": "Id of a popover menu this button toggles (`popovertarget`)."
}
},
"description": "At most one action (Button data), e.g. `{ label: \"Plan erstellen\", variant: \"primary\" }` or a link with `href`."
},
"live": {
"type": "boolean",
"default": false,
"description": "Announce politely (`role=\"status\"`) — for empty search/filter results."
}
}
}Markup & states
Root: div.empty — usable without render() by writing the markup directly.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
role="status" | — | — | Polite announcement (with live). |
Parts
| Part | Description |
|---|---|
.empty__icon | 48 px --color-surface-2 circle holding the 24 px icon in --color-text-muted (decorative). |
.empty__title | Title (p or h2–h4), --text-lg semibold. |
p | One-sentence explanation in --color-text-muted. |
.button | Optional single action. |
Accessibility
- The icon is decorative (
aria-hidden); title and text carry the message. - With
live, the region is a polite status so filter results are announced without stealing focus. - Only one action, so keyboard users find the next step immediately.