Cover All Four States
Empty, loading, error, normal. The AI writes only the last one by default.
Empty, loading, error, normal. A piece of interface shows four faces in the real world, and the AI usually builds only one of them. The other three are either blank or throw the error straight at the user.
What you'll run into:
- Your demos run perfectly; a colleague logs in and sees a white screen, because their account has no data
- On a slow connection the page collapses first, then expands when data arrives, and the button you were about to click runs away
- When the API fails, the whole page becomes an Uncaught TypeError and users refresh and hope
It's not the model's fault. Say "build an order list," and the picture in its head is a screen of neatly arranged orders, because that's all your description contains. Empty, loading, and error never appear in the spec—they only appear on real users' hands: a brand-new signup opening it for the first time, the signal dropping on the subway, the backend happening to be mid-deploy.
Filling them in usually costs a few dozen lines of code. The real hurdle is treating them as part of the spec when you write it, not patching them in after the bug reports arrive.
How to tell
Every time you fetch data, the result lands in one of four cells. Walk through them and you'll know which one you missed: data present is normal, an empty array is empty, a request in flight is loading, a failed request is error.
Empty splits one level deeper: did this person simply have no data, or did the filters wipe it out? The first should guide them to create; the second should offer relaxed filters and a one-click clear.
Design considerations
- The empty state is a chance to introduce the product—don't just write "no data." The first thing a new user sees is usually the empty state. That space should explain what will live here and how to put something there: one line of explanation plus one primary button. A bare "no data" wastes the best teaching spot on the page.
- Use a skeleton screen for loading, not a centered spinner. A skeleton reserves the space content will fill, so the page doesn't jump when data arrives. A spinner lets the whole area collapse and re-expand. Only show a loading state for waits longer than a second—shorter flashes feel worse than nothing. Material Design's guidelines cover loading and progress indicators well. progress-md
- Write error copy as "state the situation + next action." First say what happened (couldn't reach the server), then what the user can do (retry or check the network). This formula works for any message. Put the retry button next to the copy; don't make people refresh the whole page.
- The form a message takes depends on severity. Three tiers, strong to weak: urgent actions use a modal, state changes use a global banner, nice-to-know uses a toast. Skip a tier and users quickly learn to click "OK" with their eyes closed.
- A local failure gets a local message—don't collapse the whole page. A broken recommendations module in the sidebar shouldn't turn the entire page into an error screen. The granularity of states should match the granularity of data sources; each independent request manages its own region.
- Keep the container heights of all four states close. Violent height changes on state switches make the page jump, and the button you're about to click moves. A min-height on the container is the cheapest UX improvement there is.
- No exclamation marks in error copy, no trailing punctuation either. An exclamation mark turns a network blip into an incident. Waiting-state copy ends with an ellipsis ("Loading…")—the common convention in Chinese interfaces.
Note for the AI
Put it in the component spec, or sink it into the project's CLAUDE.md, and every data component generated from then on carries all four states by default:
Every component that touches async data must implement all four
states, no exceptions:
1. Normal: renders as usual when data exists.
2. Empty: when the data array is empty. Distinguish two sources—
- The user has no data yet: one line of explanation + one
primary action button
- No results after filtering/searching: suggest relaxing the
filters + a one-click clear
3. Loading: use a skeleton screen whose shape and count mirror
the real content; don't use a centered spinner.
4. Error: copy in the "state the situation + guide the action"
format, with a "Retry" button. Don't expose status codes or
stack traces.
Copy rules:
- No exclamation marks; no trailing punctuation except question marks.
- Waiting-state copy ends with "..." e.g. "Loading...".
- Message strength has three tiers: urgent use a modal, state
changes use a global banner, nice-to-know use a toast. Never skip tiers.
Other constraints:
- All four states share the same outer container with a min-height,
so switching doesn't make the page jump.
- Errors affect only the current component; don't bubble up and turn
the whole page into an error screen.
- Expose the state as a component prop ('ok' | 'empty' | 'loading'
| 'error'), so I can preview each one without touching the backend.
First tell me how you plan to present each of the four states;
only write code after I confirm.
That last line is critical. Asking it to describe the approach first lets you tell in seconds whether the empty state is yet another bare "no data"—far faster than reading the code and redoing it.
Real examples
- Notion's empty database offers a few template options and a creation entry, using that space to teach users what to do next.
- Linear's loading skeleton mirrors the real content's row height and columns, so when data arrives the switch is almost imperceptible and the page doesn't jump.
