States and feedback
"Feedback" here means the SYSTEM telling the reader what happened: empty,
loading, refused, pending. The reader telling the system — Shift+Click anything
to report it, and the queue an agent drains — is 24-feedback-and-triage.md.
Loading
Skeletons shaped like what is coming. A title bar, then a block the size of the table or the card. Never a centred spinner on an empty page — it says "something is happening" and nothing about what.
While a role-critical fact is still resolving (status === "loading"), render
nothing rather than a guess. A control that appears and then vanishes is
worse than one that arrives a beat late.
Empty
EmptyState: a dashed-border card, a 40px medallion holding a 20px icon, a
title, one sentence, and at most one action. Distinguish the three empties:
| Situation | What it says |
|---|---|
Nothing yet | What would put something here |
Nothing matches | What is filtered, and how to clear it |
Refused | Who the reader is, what the screen decides, who to ask |
Errors
Alert with three variants: default (neutral), warning (the caution
semantic, with its 3px left accent bar — the one place a coloured left border is
allowed, because it is an alert, not a callout), and destructive.
An error that stops a write gets a title naming the failure ("Not saved") and a description carrying the real message. On screen, not in a toast: a toast that disappears takes the only evidence with it.
Disabled
Three real changes — muted fill, muted ink, plain border — plus
cursor-not-allowed. Never opacity alone.
If a control is disabled for a reason, the reason must be reachable: a tooltip on the control, or a badge that replaces it. A disabled control with no explanation is a question the reader cannot answer.
Pending and unknown
A declared-but-unfilled value is not zero and not blank. Use an explicit pending
device (an em dash with a tooltip, or a Pending badge) so the reader can tell
"we have not measured this" from "this is zero".
Tooltips and hints
Tooltipfor interactive elements — a badge with a reason, a truncated label.InfoHint(the(?)) for column headers and stat labels: one sentence saying what the figure counts.titlefor extra precision on a figure (the full ISO date under "2h ago").
Never put a tooltip on something a reader cannot reach with a keyboard, and never hide a fact they need to act in one.
Motion
Transitions are colour and transform, ~150ms. prefers-reduced-motion is
honoured globally in globals.css. Motion confirms an action; it does not
announce one. One well-orchestrated reveal beats a dozen micro-interactions.