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

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.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
