Skip to content

Empty states that are not sad

An empty state that only says "No data available" is a dead end. It should say what belongs here, why it is missing, and what to do next.

Admin panel4 min readships at docs/solutions/admin-panel/empty-states-that-are-not-sad.md

Tags: ui · empty-states · admin · design · product

Every admin panel is empty on its first day, and parts of it stay empty forever (the refunds table for a product with no refunds, the error log on a good week). The empty state is therefore not an edge case. It is the state a new operator sees first, and it is doing one of two things: teaching them what this screen is for, or making them think the app is broken.

"No data available" with a grey illustration does the second one.

There are four empty states, not one

They look identical and mean completely different things. Conflating them is the root of most bad ones.

1. First run. Nothing exists yet because the feature is new. The operator needs to know what will appear here and what triggers it.

No payouts yet. Payouts appear once a payment provider is connected and the first transfer settles, usually a day after the first sale. [Connect a provider]

2. Filtered to nothing. Data exists; the current filters exclude it. The operator needs the filters, not an explanation of the feature.

No payouts match these filters. 1,284 payouts exist. Clear the date range to see them. [Clear filters]

Getting this one wrong is the most common failure: a user with 1,284 rows sees "No payouts yet" and concludes the data was deleted.

3. Genuinely, healthily empty. The refunds table on a good week. This one is good news and should read like it.

No failed payments in the last 7 days. Nothing to do here.

4. Broken. The query failed, the service is down, the policy denied the read. This must never render as an empty state: it needs an error, with a retry and something to grep for.

The distinction between 3 and 4 is worth code, not just copy. A helper that throws when a query returns an error, rather than passing an empty array onward, is what keeps a blocked read from rendering as "nothing here".

What a good one contains

A title that names the thing. "No payouts yet", not "No data". The noun tells the operator which of the four states they are in.

A sentence about why. Empty because it is new, empty because filtered, empty because all is well. One sentence, in the language of the person doing the job, not the schema.

The next action, when there is one. A link to the setting that starts the flow, a button to clear filters, a link to the doc. If there is genuinely no action, say so. "Nothing to do here" is a complete and satisfying answer.

A pointer for the confused. A link to the runbook or the cookbook entry costs one line and turns a message into an answer.

What to leave out

  • A large illustration. It fills space and says nothing. A quiet dashed border does the job of separating the region from the page.
  • A primary action the operator cannot perform. A "Create user" button that a support agent lacks permission for is a dead end with a colour.
  • Blame. "You have not configured anything" reads badly to the person who inherited the account this morning.
  • Fake rows. Skeleton placeholders that never resolve are worse than an empty state; the operator waits for data that is not coming.

Do not let it flash

A page that renders empty for a moment before the data arrives shows the empty state to everyone, every time: the "did that just say no users?" flicker. In the App Router, fetch in the Server Component so the first HTML already has the rows, and put a real skeleton in loading.tsx if the fetch is slow. The empty state renders only when the query has actually returned nothing.

Write them at the same time as the table

The empty state written a week later gets three words. The one written while the feature is fresh gets the sentence only you can write, because only you know why this table would be empty. That is why a good EmptyState component makes description required and action optional: the explanation is the part that matters, and the part that gets skipped.

Checking your work

  • With no rows and no filters: the first-run copy, naming what appears here.
  • With filters that exclude everything: different copy, and a way to clear them.
  • With the query erroring: an error, not an empty state.
  • Read each one aloud to someone who has not seen the feature. If they ask "so what do I do?", it is not finished.