Browse all of Kata

Empty, loading, error

Source docs/design/patterns/states.mdMarkdown

On this page

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

Part of the Konjo design language. 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. 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.

Use Skeleton 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 — 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
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.

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.