All articles
Design & UXSeptember 28, 2026 7 min read

Empty States Are a Product Decision, Not a Design Afterthought

Most empty states are decorative apologies. The good ones sell the product, teach the mental model, and reduce support tickets. Here's how we design and build them without shipping cartoon mascots nobody asked for.

Empty States Are a Product Decision, Not a Design Afterthought

Every product has empty states. Most treat them as a place to put a shrug emoji and a friendly sentence. That's a wasted screen — often the first screen a new user sees — and it's usually the reason your activation funnel leaks between signup and second session.

An empty state isn't a design problem. It's a product decision about what someone should do next, wrapped in a UI. Get it right and it replaces half your onboarding tour. Get it wrong and users bounce before they ever see the thing you built.

The Three Empty States People Confuse

Before anyone opens Figma, name the state. There are three, and they need different treatments.

First-use empty. The user has never had data here. This is a teaching moment. You have their attention exactly once.

Cleared empty. The user had data and now doesn't — inbox zero, all tasks done, filter returned nothing. This should feel like an accomplishment or a neutral fact, not an error.

Error-adjacent empty. The API returned nothing because something is broken, permissions are wrong, or the query is malformed. This looks like an empty state but it's actually a failure state pretending to be one.

If your component takes a single isEmpty boolean, you've already lost. The state needs enough context to render the right message.

type EmptyReason =
  | { kind: 'first-use' }
  | { kind: 'cleared'; previousCount: number }
  | { kind: 'filtered'; activeFilters: string[] }
  | { kind: 'permission-denied' }
  | { kind: 'error'; retry: () => void };

function ListView({ items, emptyReason }: Props) {
  if (items.length === 0) return <EmptyState reason={emptyReason} />;
  return <List items={items} />;
}

That discriminated union is doing real work. It forces the calling code to know why the list is empty, which forces the product team to have decided.

First-Use: Sell the Feature, Don't Decorate It

The first-use empty state is the highest-leverage screen in your app. Users landed here on purpose. They clicked "Projects" or "Integrations" because they expected something. Now you tell them what that something is.

A good first-use state has four things and nothing else:

  1. A one-line description of what lives here — not what the feature is called, what it does for the user.
  2. A single primary action — the exact next click.
  3. A hint at the shape of a filled state — a faint preview, a sample row, a screenshot of what "good" looks like.
  4. A secondary path for the curious — docs, a template gallery, an import option.

Skip the illustration budget. A restrained visual that echoes your product's shape beats a bespoke cartoon of a person holding a magnifying glass. If you must illustrate, use the same geometric language as your empty chart states and skeleton screens — consistency reads as intention.

The Sample Data Trap

A popular pattern is to pre-populate the account with sample data instead of showing an empty state. This works for note apps and dashboards. It breaks badly for anything with side effects — CRMs, project trackers, billing tools — because users don't realize the data is fake, act on it, then get confused when the fake customer doesn't reply to their real email.

If you use sample data, mark it visibly and give a one-click way to wipe it. The empty state that follows the wipe is the real first-use state, and it should be designed with the same care as the seeded version.

Cleared vs. Filtered: Different Emotions, Different Copy

Inbox zero should feel earned. "All caught up" with a subtle checkmark is fine. Don't add a CTA — the user just finished something, don't immediately ask them to start something else.

A filtered-to-nothing state is different. The user is actively looking and failed. The most useful thing you can do is tell them which filter is the culprit and offer to remove it.

<EmptyState
  title="No results for these filters"
  body={`Try removing "Status: Archived" or broadening the date range.`}
  actions={[
    { label: 'Clear all filters', onClick: clearFilters, variant: 'primary' },
    { label: 'Clear date range', onClick: clearDates, variant: 'ghost' },
  ]}
/>

Notice the primary action is destructive-to-the-filter, not destructive-to-the-page. The user's intent was to see items, not to keep their filter pristine.

Accessibility: Empty Isn't Silent

Empty states are one of the most commonly broken screens for screen reader users, because developers assume "nothing to render" means "nothing to announce." That's wrong. The absence of results is itself information.

A few rules that hold up:

  • The empty state container should have role="status" (or aria-live="polite") when it appears as a result of user action — filtering, searching, deleting the last item. Not on initial page load, or it will double-announce with the page title.
  • The heading inside should be a real heading (h2 or h3), not styled text. Screen reader users navigate by heading, and an empty state with no heading is invisible to that flow.
  • Primary actions need to be focusable and reachable from keyboard without a mouse-hover reveal.
  • Contrast for the "muted" illustration or background pattern still needs to hit 3:1 against its container if it carries meaning. If it's purely decorative, mark it aria-hidden="true" and stop worrying about it.

We've seen more than one team ship an empty state where the CTA button was rendered with opacity: 0.6 on a light background and failed WCAG contrast. Muted does not mean invisible.

Loading, Empty, and the Flash Problem

The worst empty state UX isn't a bad message — it's the empty state flashing for 200ms before real data arrives. Users read "No projects yet" and start clicking "Create project" before the list populates and steals their click.

The fix is a state machine, not a set of booleans:

type ViewState =
  | 'idle'
  | 'loading'
  | 'loaded-empty'
  | 'loaded-with-data'
  | 'error';

Only loaded-empty renders the empty state. loading renders a skeleton. idle renders nothing. If your data fetching library gives you isLoading and data, derive the state; don't render off the raw flags.

Also: debounce transitions into empty for filtered lists. If a user is typing in a search box, don't slam the empty state in after every keystroke. Wait until the query settles, or keep the previous results dimmed until new ones arrive. Perceived stability matters more than technical accuracy.

Building It Into the Design System

Empty states get inconsistent because they live at the seam between feature teams and the design system. The system ships a <Card> and a <Button>, but the empty state is "just some markup" that each team writes themselves. Six months later you have fourteen variants.

Ship a component. Keep it opinionated:

<EmptyState
  variant="first-use" // 'first-use' | 'cleared' | 'filtered' | 'error'
  title="Connect your first repository"
  body="We'll scan it for issues and open PRs when we find fixes."
  illustration={<RepoGlyph />}
  primaryAction={{ label: 'Connect GitHub', onClick: openOAuth }}
  secondaryAction={{ label: 'Browse templates', href: '/templates' }}
/>

The variant prop constrains the visual treatment. Product teams choose which empty state, not how it looks. This is the same discipline you'd apply to alerts or toasts, and for the same reason: consistency signals reliability.

Figma Parity

Mirror those variants as Figma component properties. If the code has four variants and Figma has one "Empty State" component with a text override, designers will invent visual variants that engineering can't ship. If Figma has four and code has one, engineers will approximate. Match them one-to-one, and review new variants together.

Measuring Whether It Works

Empty states are measurable, which is why it's strange how few teams instrument them. At minimum, log:

  • Impressions per empty-state variant.
  • Click-through on the primary action.
  • Time-to-first-meaningful-action for users who land on a first-use empty state.

If the first-use CTA has a click-through under roughly 30–40%, the copy or the CTA is wrong — in our experience that band is where well-designed onboarding empty states tend to land, though it varies by product surface. If cleared-empty states get frequent CTA clicks, the celebration is being read as a prompt and you should soften it.

Where We'd Start

Pick your three highest-traffic empty states — usually the main list view, the search results, and the first tab of your onboarding. For each, write down which of the three kinds it is, what the single next action should be, and what happens if that action fails. Then build one <EmptyState> component with variants, replace those three screens, and instrument the CTA.

You'll ship it in a week and learn more about your activation funnel than the last quarter of analytics reviews told you. Empty states aren't decoration — they're the parts of your product where users are most willing to be taught. Teach them something.

#UX#Design Systems#Frontend#Onboarding

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project
Empty States: Design and Engineering Patterns That Work · 72Technologies