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.
- 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.)
- Can we suggest from a real source? → suggest it, so the first tap is "yes, that one" rather than "create something."
- 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.inputfill. 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 fillborder.hairline on surface.cardis 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 fillborder.input on surface.cardis 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 isradius.card12; one standing in for a line of text is a bar atradius.input8. The chapter saidradius.inputflatly, 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.cardonsurface.pageatradius.card, withborder.inputbars 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.