Dashboard

How to Design Empty States in an AI-Built App

AI builders preview against seeded demo data, so the screen a real user sees first is the one nobody looked at. The four empty states, and how to get a builder to write them.

Steve Jefferson
Steve Jefferson
Developer Advocate
15 September 20261 min read

How to Design Empty States in an AI-Built App

An empty state is what your app shows when there is nothing to show, and in an app built with AI it is almost always the weakest screen in the product. The fix is to treat the empty screen as a real screen with its own copy and its own primary action, and to ask for it explicitly, because your builder will not produce one on its own.

The reason is mechanical rather than mysterious, and it is worth understanding before you start writing prompts.

Why AI builders get this wrong so consistently

When an AI builder scaffolds a list view, it generates seed data at the same time. Three fake customers, five fake invoices, a tidy little table. Every preview you see during the build is populated. Every screenshot in the chat is populated. The builder is never looking at the screen you are about to ship, because the screen you are about to ship has zero rows in it.

Then you deploy, a real person signs up, and the first thing they see is a heading, a search box that filters nothing, and a large area of white. There is no bug. Every test passes. The app is simply mute at the exact moment the user most needs direction.

This is the same category of problem as forgetting to check the app against real input, which is why it belongs in the same pass as testing an AI-built app before launch rather than in a polish phase afterwards.

The four empty states every app has

Most people think of one empty state. There are four, they mean different things, and a user who cannot tell them apart will assume the app is broken.

State

What happened

What the user needs

First run

The account is new and no data exists yet

An explanation of what this screen will contain, and one button to create the first item

No results

Data exists but a filter or search excluded all of it

The active filter stated plainly, and a way to clear it

Cleared out

There was data and the user deleted or archived all of it

Confirmation that this was intentional, and a route to the archive

Blocked

Data exists but this user lacks permission, or a fetch failed

The reason, and who to ask or what to retry

The failure that costs you the most is collapsing the second into the first. A user searches for a customer who is not in the system, sees your cheerful first-run message saying they have no customers yet, and concludes that their data is gone. That is a support ticket and a trust problem from a single missing conditional.

What good empty state design actually does

Nielsen Norman Group's guidance on empty state interface design is the standard reference and it comes down to three jobs. Explain what belongs here. Say how to get some. Do not make the user feel they have made a mistake.

To which, for anything built quickly with AI, add a fourth: distinguish empty from broken. A spinner that resolves into nothing looks identical to a failed request. If your fetch can fail, the failed state needs different words from the empty state, or your users will refresh until they give up.

Writing the copy

Empty state copy is short enough that people write it carelessly. A pattern that holds up:

  • One line naming what lives here, in the user's words, not the table's. Clients, not client records.

  • One line on how the first one arrives, including the route that is not the button. Add one manually or import a CSV.

  • One primary action. If there are two equally weighted buttons, you have not decided what the user should do.

  • No apology, no exclamation mark, and no illustration that takes more vertical space than the instruction.

Compare these two, for a list of client intake forms:

Bad:   No data found.

Better: No intake forms yet
        Forms appear here once a client submits one. You can
        also add a form manually while you are testing.
        [ Add a form ]  Or copy your intake link

The better version tells you what the screen is for, sets the expectation that it fills up through a route you do not control, and still gives you something to press. If the app also supports bulk loading, the empty state is the correct place to mention it, which is the natural pairing with adding CSV import to an AI-built app.

A worked example

Take a client intake list. The component needs to branch four ways before it renders a table, and the order matters: check the error first, then loading, then the filter, then the genuinely empty case.

jsx
function IntakeList({ items, loading, error, query }) {
  if (error)   return <Blocked reason={error} onRetry={refetch} />;
  if (loading) return <Skeleton rows={3} />;

  if (items.length === 0 && query) {
    return (
      <Empty
        title={`No forms match "${query}"`}
        body="Try a shorter search, or clear the filter to see everything."
        action={{ label: "Clear search", onClick: clearQuery }}
      />
    );
  }

  if (items.length === 0) {
    return (
      <Empty
        title="No intake forms yet"
        body="Forms appear here once a client submits one. You can also add one manually while you are testing."
        action={{ label: "Add a form", onClick: openForm }}
        secondary={{ label: "Copy intake link", onClick: copyLink }}
      />
    );
  }

  return <Table rows={items} />;
}

Two details that are easy to miss. The no-results state quotes the query back, which is what tells the user the app heard them. And the skeleton comes before the empty check, so a slow connection never flashes the first-run message at someone who does have data.

Getting your builder to produce them

Builders respond well to being told the states exist, because the problem is omission rather than inability. A prompt that works:

For every list, table and search view in this app, add explicit
empty states. Handle four cases separately:

1. first run, no data has ever existed
2. no results, data exists but the current filter excludes it
3. cleared, the user deleted everything
4. blocked, the fetch failed or permission is missing

Check error, then loading, then filtered-empty, then truly empty,
in that order. Each state needs a heading, one sentence of body
copy, and at most one primary action. Do not use seeded demo data
to preview these. Render each state with an empty array.

The last line does the heavy lifting. Without it the builder will confirm the states are handled while still previewing against its own seed data, and you will find out otherwise in production.

Ask it to show you each state as a separate screenshot or route before you accept the change. This is the same discipline as asking an AI coding agent to explain its plan before it edits, applied to a UI surface.

Accessibility and the empty screen

An empty state is usually the one screen with no landmarks, which makes it a common screen reader dead end. Give the container a heading that is a real heading element rather than styled text, announce loading and error transitions through a live region, and make sure the primary action is reachable by keyboard from the page's first tab stop. The broader checklist is in making an AI-built app accessible.

Frequently asked questions

Do I need an illustration in my empty states?

No. An illustration is optional decoration and it competes with the instruction for attention. If you use one, keep it small enough that the heading and the button are both visible without scrolling on a phone.

Should the empty state be the same as the onboarding flow?

They overlap but they are not the same. An onboarding flow runs once and explains the product. An empty state recurs whenever a list is empty, including three months in after someone archives everything. Build them separately and let the empty state be the shorter one.

What about a dashboard with several empty widgets?

Do not repeat the same message six times. Give the page one empty state that covers the whole dashboard when nothing has data yet, and let individual widgets show a compact zero value once any data exists.

How do I test empty states without deleting my data?

Create a second account with nothing in it and keep it. It costs nothing, it takes thirty seconds to make, and it is the only reliable way to see what a new user sees after you have been using your own app for a month.

Is this worth doing before launch?

Yes, because every single user sees the first-run state and they see it before they see anything else. It is the highest-traffic screen in the product on day one and usually the least considered.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.