<!-- https://getkonjo.com/design/patterns/states · source: docs/design/patterns/states.md -->

# Empty, loading, error

The three states every screen owes the person looking at it.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine apply here unless this chapter contradicts them.

**Empty — ask first whether the screen should be empty at all.**

1. Is there real data elsewhere that belongs here? → show it. (Run class already knows
   tonight's techniques from the class plan; it should list them ungraded, not show a void.)
2. Can we suggest from a real source? → suggest it, so the first tap is "yes, that one"
   rather than "create something."
3. Genuinely nothing yet? → then the standard empty block:

```
NOTHING GRADED YET              eyebrow
Add what you're teaching        title 22/700
Pick techniques from tonight's  body — what goes here and why
plan and grade as you go.
[ Add from class plan ]         one filled primary
Add something else              one text link
```

Left-aligned, in flow, never centred in a void. The secondary action on the screen
(e.g. "Wrap up") **visibly demotes** while the empty state is showing — two red buttons of
equal weight is the current Run class bug.

**Loading:**

| Wait | Show |
|---|---|
| < 100ms | nothing |
| 100ms – 1s | nothing, or optimistic UI |
| 1 – 10s | a spinner; a skeleton **only** where the layout is predictable and repeated (feed rows the user has seen before) |
| > 10s | determinate progress, what is happening, expected completion |

Skeletons are not free. A 136-participant study (Viget, 2017) found skeletons performed
*worst* of skeleton / spinner / blank on every metric — 59% agreed loading felt fast vs
74% for a plain spinner. Use them where they model a real, familiar layout; nowhere else.
Never a "frame-display" skeleton (header and footer only, no content placeholders).

**Error** — same anatomy as Empty, same left-aligned block, never centred in a void:

```
COULDN'T LOAD                   eyebrow
Couldn't load events            title 22/700
Check your connection and       body — plain words, what failed, no error codes
try again.
[ Try again ]                   one filled primary
```

The filled-primary budget is usually free here, because a screen that failed to load has no
hero. Field-level errors sit inline below the offending field instead, and fire on blur —
never on keystroke.


## Skeletons

The only rule the system carried was "skeleton fill `border.hairline`", which is not enough to
build one. The rest:

- **A skeleton is the shape of the thing that is coming**, at its final height, in its final
  position. If it does not match, the content jumps when it arrives and the skeleton has made
  the wait worse rather than shorter.
- **`border.input` fill.** Text lines are the line-height of the token they stand in for; a
  one-line label is a bar about 60% of the container's width, a paragraph is two or three bars
  with the last one shorter.

  A skeleton owes no contrast ratio — it carries no information and is hidden from assistive
  tech — but it does owe being *seen*, which is its entire job. The fill was `border.hairline`,
  and that was a rule tuned on one surface and published as a universal: the old skeleton fill
  `border.hairline on surface.card` is 1.22:1 — faint but there; on the page it fell to
  1.09:1, so every skeleton that was not inside a card was invisible. The new placeholder fill
  `border.input on surface.card` is 1.42:1, and on the page 1.27:1, and it holds in dark.
- **The radius is the radius of the thing that is coming**, not a fixed `radius.input`. A
  skeleton standing in for a card is `radius.card` 12; one standing in for a line of text is a
  bar at `radius.input` 8. The chapter said `radius.input` flatly, three lines under its own
  rule that a skeleton is the shape of the thing coming, and the two cannot both be true.
- **A skeleton for a whole card is a card**, not a bar: the placeholder is `surface.card` on
  `surface.page` at `radius.card`, with `border.input` bars inside it. The card's own tone separates
  it, exactly as the real card's will be.
- **No pulse, no shimmer, no sweep.** The depth law forbids gradients, and an animated skeleton
  is a moving gradient. It is also motion that carries no information — see
  [motion](../foundations/motion.md). If you ever do animate one, it checks
  `useReducedMotion()`.
- **Only where the layout is predictable and the person has seen it before.** A skeleton for a
  screen someone is opening for the first time teaches them nothing; use nothing under a
  second, then a spinner.
- **Never mix a skeleton and real content in the same row.** Either the row is known or it is
  not. A half-loaded row reads as broken.
- **Skeletons are hidden from screen readers** — `accessibilityElementsHidden`. Announce the
  load's completion instead, per [accessibility](../foundations/accessibility.md).

Use [`Skeleton`](../../../src/components/ui/Skeleton.tsx) from the barrel — `Skeleton.Text`
for a line of type, which takes its height from the token it stands in for, and
`Skeleton.Block` for anything with its own dimensions. Do not compose a second set by hand;
two feature-local skeletons already exist and are exactly the drift this warns about.

## A section is not a screen

Everything above describes a screen. A **section** inside one — an empty "Recent", a details
card that failed to load — follows the same anatomy at a lower weight, because the screen
around it is still doing its job.

- **A state's action inherits the weight of what it replaced.** A failed *section* sits under a
  screen whose filled primary is still on screen, so its retry is a **bordered secondary**. A
  failed *screen* renders nothing else, so its retry **is** the filled primary. The
  one-filled-primary budget holds either way, and this is the rule that resolves the apparent
  conflict between it and "a retry is the filled primary".
- **A section-scoped empty offers no action at all if the action is already on screen.** Text
  only, in flow, left-aligned: what goes here, and why it is not here yet. Repeating a button
  that already exists 200pt above puts the same destination twice in one viewport, which is
  worse than a missing button.
- **A section that fails never takes down the screen.** The rest of the record still renders.
- **A section with genuinely nothing to say does not render** — no "None", no em dash, no empty
  card. Its absence is the information. The exception is an emptiness that is *actionable*: a
  missing waiver earns a row saying so.
- **This rule is for a detail screen, whose sections vary by record.** On a fixed grid — a
  [Studio dashboard](../studio/dashboard.md) — a card that vanishes on a quiet day changes the
  page's shape every morning, and the person can no longer learn where anything is. There, a
  region with nothing to say prints one line saying so.

## Empty is not "not set up"

There are **three** ways a screen can have no data, and treating the third as the first is the
design lying to the person.

| | What it means | What it says |
|---|---|---|
| **Empty after filtering** | The data exists; this query excludes it | Name the filter that is excluding everything, and offer to clear it. See [search and filter](search-filter.md) |
| **Empty collection** | The feature works; nothing has happened yet | What goes here, and what makes the first one appear |
| **Not yet set up** | The feature was never switched on | **What is off, and where the switch is** |

**Not-yet-set-up is the actionable-emptiness exception**, so it earns one line and one action —
a text link, at the weight of what it replaced, never a filled primary:

> Lead capture isn't turned on. · `Set up your public page →`

- **Never render it as data.** "0 leads this month" from a dojo that has no lead form is a
  false statement about the business, and an owner will act on it.
- **Never render it as an error.** Nothing failed. A red block over a feature nobody has
  turned on teaches people to ignore red.
- **The distinction is a product question, not a design one**: whether a feature counts as
  "set up" is a business fact, and if it is not written down, name it rather than guessing at
  a heuristic. See [product facts](../product-facts.md).

A new dojo's first week is almost entirely this state, and it is the only week where every
screen is seen for the first time.
