# CMP-4 — Every empty-state view unambiguously signals "no content exists" — distinct from loading, error, or permissions failure — through a heading, explanatory subtext, and the absence of loading chrome such as skeleton rows or spinners

L1 · judgment · Components & patterns

**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

## 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:

1. **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.
2. **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.
