Every mockup in the design review had twelve projects in it. The demo account has eighteen months of history. The engineer building the list view seeded it with realistic data on the first afternoon, because otherwise there was nothing to look at.
So the screen that every single new user meets first, the one with nothing in it, is the only screen in the product that nobody designed, nobody reviewed, and nobody has looked at since it was a placeholder. It usually says “No results” in grey, centred, with a lot of space around it.
Four different screens wearing the same clothes
The reason “No results” is unsatisfying everywhere is that it is being asked to serve four situations that have almost nothing in common. Separating them is most of the work.
- First run. The user is new and has created nothing yet. This is not an error, it is the beginning. It is the only one of the four where the screen’s job is to teach.
- Filtered to nothing. There is data; the current query excludes it. The user needs to know that, and needs the way back out. Showing them a first-run tutorial here is insulting.
- Genuinely cleared. The inbox is empty because they dealt with everything. This is a success state and it should feel like one.
- Something failed. The request errored and the list rendered empty because the array was empty. This is the dangerous one, because a failure disguised as an empty state teaches the user that the product has no data rather than that it is broken.
The fourth is worth a specific check in code review. If your data layer returns an empty collection on error, every empty state in the product is silently also an error state, and your support team is fielding “where did my projects go” tickets that no dashboard will ever explain.
// The bug that makes every empty state a liar
const { data } = useQuery(...)
return <List items={data ?? []} /> // error and empty are now the same
// Three states, not two. The UI cannot be honest without them.
if (error) return <Failed onRetry={refetch} detail={error} />
if (loading) return <Skeleton rows={3} />
if (!data.length) return <Empty variant={filtersActive ? "filtered" : "first-run"} />
return <List items={data} />
First run is not a tutorial
The common response to a bare first-run screen is to add a product tour: a sequence of tooltips pointing at things the user cannot yet use, ending in a button that says Got it. Completion rates for these are poor and the reason is not mystery. The tour explains the interface before the user has a reason to care about any part of it.
A first-run screen does better with three things and nothing else. Say what this space will hold, in the user’s language rather than the schema’s. Offer exactly one action, the one that produces the first real object. And where the product allows it, offer sample data that is clearly marked and trivially removable, because a person understands a populated list in two seconds and understands a description of a populated list not at all.
The single action is the part teams find hardest, because every stakeholder has a candidate. The discipline is to pick the action that most reliably precedes the user coming back, which is a question your data can answer and your opinions cannot.
Filtered-to-nothing needs an exit, not encouragement
When a query excludes everything, the user has a specific problem: they cannot see what they narrowed, and they do not know whether the fault is the filter or the data. Three things resolve it, and all three are usually missing.
Restate the query in the empty state, in words: no invoices from Northwind, marked overdue, in the last 30 days. Say how much is behind it: there are 412 invoices in total. And give a one-click way back: clear the date filter, clear everything.
A quiet extra that pays for itself: log which filter combinations produce zero results most often. It is one of the cheapest sources of product insight you have, because a filter that constantly returns nothing is either a missing feature, a mislabelled control, or a data problem, and all three are worth knowing about.
Cleared is a success, so let it look like one
An empty inbox after real work is one of the few genuinely satisfying moments a tool can produce, and most products squander it by rendering the same grey “No items” they show a stranger on day one.
It costs almost nothing to distinguish. Different copy, a different weight, and something that acknowledges what happened rather than describing the absence. The restraint worth keeping is proportion: acknowledge, do not celebrate. A confetti animation for clearing four tasks is a product being pleased with itself, and it gets tiresome by the third time.
The measurement that tells you which problem you have
Most teams measure activation as a single funnel: signed up, then did the thing. That merges two very different failures and hides which one you have.
Split it at the empty state. Count the users who reach the first-run screen, then the users who create one object, then the users who come back and find something waiting for them. The gap between the first two is an onboarding problem, fixable in the empty state itself. The gap between the second and third is a value problem, and no amount of empty-state copy will touch it.
Teams that only track the combined number spend quarters redesigning the wrong screen. It is worth knowing which half is leaking before anyone opens the design file.
The audit worth doing
Create a genuinely new account, with no seed data and no fixtures, and walk every screen in the product. Not the demo account. Not staging with last month’s dump in it. An account that has never had anything in it.
Write down what each screen says, and mark which of the four states it is actually handling. The usual result is that one or two screens were designed for this and the rest inherited a default that has been shipping since the first sprint. That list is a week of work with a measurable effect on activation, and it is the cheapest such list most products have available.

