---
title: "States"
description: "An answer always carries its source, its version binding, and what it leaves out."
canonical: states.html
navigation-title: "States"
---

<a id="states"></a>

# States

- [Focus](#focus)
- [Interaction](#interaction)
- [Loading](#loading)
- [Empty and not found](#empty-and-not-found)
- [A status on a word](#a-status-on-a-word)
- [Errors and degraded answers](#errors-and-degraded-answers)
- [A message with an action](#a-message-with-an-action)

An answer always carries its source, its version binding, and what it leaves
out. These states carry exactly that. None of them is a spinner with a
shrug.

<a id="focus"></a>

## Focus

An outline `--border-emphasis` wide in `--accent`, `--focus-offset`
off the box, plus a halo `--focus-halo` wide in `--accent-ring`. That is
2px, 2px and 3px, and every surface that draws the ring reads the three
tokens, not the numbers. Always `:focus-visible`, never `:focus`: a click
must not leave a ring behind.

Nothing in this system is reachable by pointer only.

[Focus & keyboard · 700x230](../_cards/guidelines/states-focus.card.html)

<a id="interaction"></a>

## Interaction

A state change uses `--duration-fast`, 140ms, with `--ease-out`. Hover
changes colour, border or fill, and never a component's size. Nothing scales
or bounces.

A press takes those colours one step further. `--accent-active` under a
filled control, the sunken plane under one with no fill at rest, and nothing
moves under the finger. No specimen can hold that state, and a reader notices
it soonest. A control that looks the same under a press reads as dead on
every page that takes longer than a frame to answer.

A linked card is the one positional exception. It rises 2px, takes the raised
fill and lights the top of its frame with `--accent-glow`. Keyboard focus
gets the same response, because the whole card is the target. A card in a
flush wall stays put, so it does not tear the shared hairlines. Reduced
motion holds every card still and keeps the fill, border and glow.

A disabled control keeps its colours at half opacity. A disabled state with a
new hue reads as a different tone. Opacity makes the control unavailable
without a new meaning.

<a id="loading"></a>

## Loading

Nothing under 200ms: a flash of loading UI reads as a state change, not as
progress. Over 2s the label says *why*: "booting the installation", "reading
packages instead", "searching docs.typo3.org". A skeleton only where the
shape is certain.

A table has a certain shape, and `sds-table` draws its own skeleton. The
head it already has, with bars at the row height under it, so nothing moves
when the rows arrive. `loading` and `loading-rows` are the whole of it;
see [sds-table](../frontend/components/data.md#component-sds-table).

**A spinner claims no distance; a bar claims one.** Work that reports how far
it got is `sds-progress`. A filled length with the position beside it as a
number, driven from outside, and it moves only when the work says so. The ink
comes from that same distance. Grey at the start, and `--status-ok` as the
run approaches a result. Never through amber or red, which say something went
wrong with a job that is only early.

Where reports arrive far apart, `pulsing` sends a hatch through the filled
part, so a slow run and a stalled one look different. Reduced motion keeps
the hatch and stops its movement. Work that reports nothing gets the spinner:
a bar on a timer tells the reader something nothing measured.

[Loading · 700x273](../_cards/guidelines/states-loading.card.html)

<a id="empty-and-not-found"></a>

## Empty and not found

> [!WARNING]
> **Never "no results".** Name the source, say it answered, say what it does
> not cover, and offer the nearest real thing. A deliberate boundary is not
> a failure. It gets `actions-info-circle`, not an error colour.

[Empty & not found · 700x419](../_cards/guidelines/states-empty.card.html)

<a id="a-status-on-a-word"></a>

## A status on a word

`.sds-ok`, `.sds-warn` and `.sds-error` put a status colour on a word:
a count in a table cell, a state in a line of prose. They set the colour and
nothing else.

Use one where a badge is a pill around a single word. `sds-badge` is the
mark on a *thing*, a row, a card, a heading, and it draws a box. In the
middle of a sentence or a cell, a box is furniture. The classes are the same
three colours as the badge, without the box.

They are the only way a page can colour a word. A page that writes \``color:
var(--status-warn)`\` into its own stylesheet holds a copy of this layer's
value. The two drift when the palette moves.

[Status on a word · 700x345](../_cards/guidelines/states-tone.card.html)

<a id="errors-and-degraded-answers"></a>

## Errors and degraded answers

Warning, error and success share one note structure: an icon, a direct
heading and an explanation. The structure identifies feedback. Colour and
icon identify its tone, so colour never carries the difference alone.

A **warning** is a degraded but usable answer. What the tool reached, what it
read instead, what that leaves out, and the command that fixes it. An
**error** is no answer, plus the command or environment variable that changes
that.

**Success** appears only when the *source* matters: "answered from bundled
knowledge · 12.4, 13.4". Never praise, never a "done" toast.

[Errors & degraded answers · 700x470](../_cards/guidelines/states-error.card.html)

<a id="a-message-with-an-action"></a>

## A message with an action

Where the reader can settle what the note says on the page, the note carries
the answer. `action` puts one button after the sentence, and `href` makes
it a link. One, and only where the press is the whole answer. A message that
offers a choice between two things is a dialog. A note with a button nobody
needs is a sentence dressed up as urgent.

The button belongs to the note, not to whoever writes the message. So every
message in a product offers its answer as the same control in the same place.
It is a secondary. The note is not what the page is for, and a filled button
in it outranks what the reader came to do.
