Every screen needs a state matrix: loading, empty, error, offline, partial, first-run
A principle and checklist for designing all states of each app screen before styling the happy path, with the minimum content for each state and a state model that makes impossible combinations unrepresentable.
Overview
What: A per-screen matrix of states that must be designed and implemented: first-run, loading, loaded (normal), empty, no-results, partial (some data failed), error, offline/degraded, and permission-denied where relevant.
Why: The happy path is a minority of real sessions. Apple's HIG says never make people stare at nothing while content loads and never show an alert at launch for problems you can show inline; NN/g stresses that blank screens make users unsure whether the system is working. The Motif blueprint requires loading, empty, permission-denied, offline/degraded and recoverable-error behaviour on every screen.
Use when: specifying or reviewing any screen.
Not optional for list, detail, form and settings screens.
Implementation
- Write the matrix first (columns = states, rows = screens). Minimum content per state:
| State | Must show |
|---|---|
| First-run | what this area is for + primary create action |
| Loading | skeleton of the real layout (or nothing if < ~300 ms) |
| Empty | why empty + how to fill it |
| No results | query/filters echoed + broadening actions |
| Partial | loaded data + inline notice for the failed part + retry |
| Error | plain cause + what's safe + Retry / alternative |
| Offline | cached data + "Offline — changes will sync" or "Needs connection" |
| Permission denied | what's unavailable + how to enable |
- Model state as a discriminated union, not booleans:
type ListState =
| { kind: 'loading' }
| { kind: 'ready'; items: Item[]; stale?: boolean }
| { kind: 'empty' }
| { kind: 'error'; error: AppError; items?: Item[] };
This makes "loading and error at once" impossible and forces each screen to render every kind.
- Local-only apps: there is still loading (DB open/migration), empty, error (disk full, migration failure) and no-results — but no offline state. Don't invent network messaging for local data.
- Preserve context in error states: keep previously loaded content visible with an inline banner instead of replacing it with a full-screen error.
- Build a state gallery: a dev-only route (e.g.
app/_dev/states.tsx) or Storybook that renders each screen in each state with fixture data, used for screenshots and review. - Copy: every state has a headline, one sentence of explanation, and at most one primary action.
Rubric wiring and the five required states (added in 1.1.0). The design gate requires loading, empty, error, offline and success designed on every key screen; any one undesigned on a shipped key screen is hard-fail HF-A8. Score anchors: 3/5 = all five designed with skeletons matching final layout; 4/5 = first-use empty states offer the first action, errors are recoverable with retry, offline shows cached content and queues actions; 5/5 = state design carries the product's voice and trust posture (weighted success confirmation, honest degradation). Two functional-truth rules also count as state defects: a declared count rendered short without a truncation affordance (HF-A12), and a visualisation that is not proportional or not legible at render size (caps state completeness at 2) — see app-functional-truth-rulings. Record the per-screen state design in the note's "State design" field.
Verification
- Each screen has a completed matrix; unsupported states are marked N/A with a reason.
- A state gallery renders every screen × state; screenshots exist in light and dark.
- Forcing failures (airplane mode, corrupt DB fixture, denied permission) produces the designed states, not blank screens or raw error text.
- No state shows a network message in a local-only app.
- Screen readers announce state changes (e.g. "No results", "Couldn't load").
- The note's state field lists all five states per key screen; counts and charts satisfy the functional-truth rules.
Sources:
- https://developer.apple.com/design/human-interface-guidelines/loading
- https://developer.apple.com/design/human-interface-guidelines/alerts
- https://www.nngroup.com/articles/empty-state-interface-design/
- https://www.nngroup.com/articles/skeleton-screens/
- https://developer.apple.com/design/human-interface-guidelines/loading
Limitations
- The matrix grows multiplicatively with multi-pane layouts; prioritise states by frequency and harm.
- Some states merge in simple apps (first-run ≈ empty); merge deliberately, don't skip.
- A dev-only state gallery must be excluded from production builds.
- Partial and first-run states from this matrix are refinements of the five required states, not substitutes for them.
- Values verified 2026-09-26: all 1 numeric values in this item were checked (1 against published design guidelines) and confirmed; no corrections were needed.