Feedbackstable.save-status
Save status
A small live indicator next to editable content: saved, saving, waiting to sync, or failed.
The feedback for safe actions that save immediately (UX guide §5.1): attendance, stripes, notes, settings, drafts. Instead of a toast per change, one quiet status near the content or in the page header tells the truth about saving.
States follow the offline outbox (UX guide §7.6): saving while a request runs, saved when the server confirmed (may fade after 2 s), pending when changes are queued offline ("3 Änderungen warten"), error when something needs a person — then place an action next to it that opens the failed items ("Erneut senden" / "Verwerfen").
Look (design guide §9.4): --text-sm text with a 16 px icon — "Gespeichert" with check in --color-text-muted, "Speichert …" with a spinner, waiting offline with wifi-off in --color-warning-text, "Nicht gespeichert" with alert-circle in --color-danger-text. All three icons are rendered; CSS shows the one that matches data-state.
Update data-state and the text of .save-status__text in the same element (don't replace the whole content — the icons live there too); it is a polite live region, so changes are announced without moving focus.
Examples
Saved
Data
render({
"state": "saved"
})Markup
<span class="save-status" data-state="saved" role="status"><span class="save-status__icon" data-for="saved"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span><span class="save-status__icon" data-for="pending"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#wifi-off"/></svg></span><span class="save-status__icon" data-for="error"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-circle"/></svg></span><span class="save-status__text">Gespeichert</span></span>Saving
Data
render({
"state": "saving"
})Markup
<span class="save-status" data-state="saving" role="status"><span class="save-status__icon" data-for="saved"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span><span class="save-status__icon" data-for="pending"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#wifi-off"/></svg></span><span class="save-status__icon" data-for="error"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-circle"/></svg></span><span class="save-status__text">Speichert …</span></span>Queued offline
Data
render({
"state": "pending",
"count": 3,
"id": "attendance-status"
})Markup
<span class="save-status" id="attendance-status" data-state="pending" role="status"><span class="save-status__icon" data-for="saved"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span><span class="save-status__icon" data-for="pending"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#wifi-off"/></svg></span><span class="save-status__icon" data-for="error"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-circle"/></svg></span><span class="save-status__text">3 Änderungen warten</span></span>Failed
Data
render({
"state": "error"
})Markup
<span class="save-status" data-state="error" role="status"><span class="save-status__icon" data-for="saved"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span><span class="save-status__icon" data-for="pending"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#wifi-off"/></svg></span><span class="save-status__icon" data-for="error"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-circle"/></svg></span><span class="save-status__text">Nicht gespeichert</span></span>Draft (custom text)
Data
render({
"state": "saved",
"label": "Entwurf gespeichert"
})Markup
<span class="save-status" data-state="saved" role="status"><span class="save-status__icon" data-for="saved"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span><span class="save-status__icon" data-for="pending"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#wifi-off"/></svg></span><span class="save-status__icon" data-for="error"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-circle"/></svg></span><span class="save-status__text">Entwurf gespeichert</span></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 |
|---|---|---|---|---|
state | "saved" | "saving" | "pending" | "error" | required | — | Current save state; sets colour and the icon/spinner. |
count | integer | optional | — | Number of queued changes (state pending): "3 Änderungen warten".≥ 1 |
label | string | optional | — | Custom text, e.g. "Entwurf gespeichert". Defaults: "Gespeichert", "Speichert …", "Wartet auf Verbindung" / "{n} Änderungen warten", "Nicht gespeichert". min 1 chars |
id | string | optional | — | Id so page code can find and update the element. pattern ^[A-Za-z][\w-]*$ |
JSON Schema
{
"type": "object",
"additionalProperties": false,
"required": [
"state"
],
"properties": {
"state": {
"type": "string",
"enum": [
"saved",
"saving",
"pending",
"error"
],
"description": "Current save state; sets colour and the icon/spinner."
},
"count": {
"type": "integer",
"minimum": 1,
"description": "Number of queued changes (state `pending`): \"3 Änderungen warten\"."
},
"label": {
"type": "string",
"minLength": 1,
"description": "Custom text, e.g. \"Entwurf gespeichert\". Defaults: \"Gespeichert\", \"Speichert …\", \"Wartet auf Verbindung\" / \"{n} Änderungen warten\", \"Nicht gespeichert\"."
},
"id": {
"type": "string",
"pattern": "^[A-Za-z][\\w-]*$",
"description": "Id so page code can find and update the element."
}
}
}Markup & states
Root: span.save-status — usable without render() by writing the markup directly.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
data-state | saved | saving | pending | error | — | Colour and indicator. |
role="status" | — | — | Polite live region. |
Parts
| Part | Description |
|---|---|
.save-status__icon[data-for="saved|pending|error"] | State icons (check, wifi-off, alert-circle, decorative); only the one matching data-state is shown. |
::before | Spinning ring while saving. |
.save-status__text | The status text; page code updates it. |
States
| State | Description |
|---|---|
[data-state="saved"] | Muted text, check icon. |
[data-state="saving"] | Muted text, spinner. |
[data-state="pending"] | Warning text colour, wifi-off icon. |
[data-state="error"] | Danger text colour, alert-circle icon. |
Behaviour & events
Module: (page code)
No built-in behaviour. The page (or the offline outbox) updates data-state and the text of .save-status__text as saves start, finish, queue or fail.
Accessibility
- State is in the text ("Nicht gespeichert"), not only in the colour or icon; the icons are
aria-hidden. role="status"announces changes politely; don't update it on every keystroke — only when a save starts or ends.
Guidance
pending is normal.Related: Offline banner · Toast · Stripe chips · Switch