Save status

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

Gespeichert
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

Speichert …
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

3 Änderungen warten
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

Nicht gespeichert
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)

Entwurf gespeichert
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.

FieldTypeRequiredDefaultDescription
state"saved" | "saving" | "pending" | "error"required—Current save state; sets colour and the icon/spinner.
countintegeroptional—Number of queued changes (state pending): "3 Änderungen warten".
≥ 1
labelstringoptional—Custom text, e.g. "Entwurf gespeichert". Defaults: "Gespeichert", "Speichert …", "Wartet auf Verbindung" / "{n} Änderungen warten", "Nicht gespeichert".
min 1 chars
idstringoptional—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

AttributeValuesDefaultDescription
data-statesaved | saving | pending | error—Colour and indicator.
role="status"——Polite live region.

Parts

PartDescription
.save-status__icon[data-for="saved|pending|error"]State icons (check, wifi-off, alert-circle, decorative); only the one matching data-state is shown.
::beforeSpinning ring while saving.
.save-status__textThe status text; page code updates it.

States

StateDescription
[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

DoPut it where the user looks while editing (header of the panel, next to the stripes).
DoShow the pending count so people know nothing is lost.
Don'tDon't use an error dialog for offline — pending is normal.
Don'tDon't combine it with a toast for the same save.

Related: Offline banner · Toast · Stripe chips · Switch