Requirement
Every empty-state view must unambiguously signal "no content exists" to a user encountering the surface cold. Two things carry that signal: a heading and explanatory subtext that read as "nothing here yet" (not "still loading" and not "you don't have access"), and the total absence of loading chrome — no skeleton rows, no shimmer, no spinner — anywhere alongside that heading. An empty state that still carries leftover loading affordances is not designed; it is a loading state that forgot to finish.
Rationale
The triggering incident is not a defect but a gap: a notes-page loop run's page
handled its own empty state well ("No notes yet" + explanatory subtext, no list chrome
that could read as loading) — the central done-criterion of that run's sprint contract —
yet no catalog control graded it directly; the pass rode entirely on CMP-3 (async states)
and evaluator judgment. The design-evaluator (opus), reading independently, confirmed the
same gap in its UNCOVERED section and recommended a ratchet proposal. The catalog carried
a reserved slot for exactly this proposal (catalog.yaml ~line 397) from 2026-06-16 until
this control filled it on 2026-07-08.
CMP-3 requires that loading/success/error states exist and are perceivable; it does not require that the empty state, once reached, actually reads as empty rather than as an interrupted loading state. CMP-4 names that specific failure mode so it is caught by rule, not by whichever reviewer happens to notice the leftover skeleton row.
Passes when
- The empty-state view shows a heading and subtext that clearly say "nothing here yet" (or the surface's equivalent plain-language phrasing) — not "loading…" and not a permissions-denied message.
- No skeleton row, shimmer placeholder, or spinner is present in the DOM while that heading is visible.
- Any list chrome present (dividers, empty row templates) reads as intentionally empty, not as a populated list still waiting for data.
Fails when
- A skeleton row, shimmer, or loading spinner is visible in the DOM alongside the empty-state heading.
- The heading/subtext pair reads as "still loading" or "you don't have access" rather than "nothing here yet."
- An empty state ships with list chrome (row dividers, placeholder rows) that could be mistaken for a populated-but-loading list.
Why this is judgment
Both readings this control needs are the evaluator's, so the evaluator is the enforcement rather than half of it.
Co-presence looks mechanical and is not. A skeleton row is only a finding when it renders while the empty-state heading is visible, and whether those two ever co-exist depends on the runtime state a component reaches, not on what its source declares. A component can hold both branches and never show them together. The second reading — whether the copy could be mistaken for a loading state or a permissions error — is a reading of language, with no mechanical form at all.
Evaluator guidance
One check, two readings, both yours:
- Read the rendered view at the moment the empty-state heading is visible. Confirm no skeleton-row element and no loading-spinner element render alongside it. Name the state you put the surface in to see it.
- Read the heading and subtext together and ask, "could a first-time user mistake this for a loading state or a permissions error?" Pass = no plausible confusion; fail = any reasonable reading supports the loading-or-error interpretation. Quote the heading/subtext text you judged.
This control applies whenever a page or component can render an empty state — most often lists, tables (CMP-6 already names "design the empty and loading states" as part of the table pattern; this control is the general rule that bullet defers to), and dashboards with no data yet.
Do not flag
- A loading state itself, correctly labelled and still loading — CMP-4 only binds the empty state, once the loading transaction has actually completed with nothing to show.
- An empty state that omits subtext but whose heading alone is unambiguous (e.g. "No notes yet — the class hasn't received any"). The requirement is clarity, not a fixed heading + subtext template.
- A skeleton or spinner that renders only during the loading transition and is fully gone before the empty-state heading renders — the fail condition is co-presence, not that a loading pattern exists anywhere in the surface's code.