All articles
Design & UXAugust 9, 2026 6 min read

Empty States Are Product Surface, Not Filler: A Practical Playbook

Empty states are the first thing new users see and the last thing designers polish. Here's how to treat them as a real product surface instead of a placeholder illustration with a sad robot.

Most teams spend weeks polishing the happy path and thirty minutes on the empty state. Then they wonder why activation is flat, why the free trial converts poorly, and why support tickets keep asking "how do I start?" Empty states are not filler โ€” they're the first honest conversation your product has with a user.

This is the playbook we use at 72Technologies when we audit or build zero-data screens, and the specific patterns that actually move activation instead of just looking cute in Dribbble shots.

Why empty states deserve real design budget

An empty state is the union of three uncomfortable moments:

  • First run. The user just signed up. They have no data, no context, and no reason to trust you yet.
  • Cleared state. They archived the last item, deleted the last project, or filtered everything out. The screen is empty because they made it that way.
  • Error-adjacent state. The API returned zero results, or a fetch failed and the fallback rendered as "nothing."

Each of these needs a different answer. Shipping one generic "No items yet ๐ŸŽ‰" component across all three is the design equivalent of a 500 error page that says "Oops!" โ€” technically present, functionally useless.

The cost of getting it wrong

In our experience auditing onboarding flows, teams that treat the first-run empty state as a real surface โ€” with sample data, clear next actions, and a genuine narrative โ€” tend to see meaningfully better week-one retention than teams that ship a static illustration with a single "Create your first thing" button. We won't quote a specific number because it depends heavily on the product, but the delta is usually large enough that it justifies a full design sprint on empty states alone.

The three types of empty, and what each one needs

Before you touch Figma, classify the screen. The pattern follows the type.

1. First-run (zero to one)

The user has never had data. Your job is to reduce ambiguity about what the product actually does. This is not the place for a witty one-liner. It's the place for:

  • A concrete example of what a populated screen looks like (sample data, preview cards, a demo project).
  • One primary action, and one only. "Import from CSV," "Create sample project," or "Connect your first source."
  • A secondary escape hatch: a link to docs, a short video, or a "show me around" tour.

The worst pattern here is the lone centered button on a white canvas. It tells the user nothing about what will happen after they click.

2. User-cleared (returned to zero)

They had data. They don't now. Assume competence. This user knows what the product does โ€” they just archived everything, or their filter matched nothing.

What they need is confirmation and reversibility:

  • Acknowledge why it's empty ("No tasks match the filter Assigned to me + Done").
  • Offer a fast undo or filter reset.
  • Do not re-teach the product. They already know.

3. Zero-result (search / filter)

Almost the same as user-cleared, but the fix is different. The user is asking a question and the answer is "none." Show them:

  • The exact query that returned nothing.
  • The closest thing you do have. Fuzzy matches, related tags, or a broadened search.
  • A way to save the search so it notifies them if a match appears later (this is a killer feature in B2B tools).

A component API that doesn't fight you

One reason empty states get neglected is that most design systems ship a single <EmptyState /> component with a fixed slot layout. Then engineers hit a case where they need two actions, or a preview, or an inline form, and they bail out to a custom div.

Here's the shape we've landed on. It's opinionated but flexible:

type EmptyStateProps = {
  variant: 'first-run' | 'cleared' | 'no-results' | 'error';
  title: string;
  description?: string;
  primaryAction?: { label: string; onClick: () => void };
  secondaryAction?: { label: string; href: string };
  preview?: React.ReactNode; // sample data, illustration, or demo
  context?: React.ReactNode; // "Filter: Assigned to me", chip row, etc.
};

export function EmptyState({
  variant,
  title,
  description,
  primaryAction,
  secondaryAction,
  preview,
  context,
}: EmptyStateProps) {
  return (
    <section
      role="status"
      aria-live="polite"
      className="flex flex-col items-center gap-4 py-12 text-center"
      data-variant={variant}
    >
      {preview && <div className="mb-2 opacity-90">{preview}</div>}
      {context && <div className="text-sm text-muted">{context}</div>}
      <h2 className="text-lg font-medium">{title}</h2>
      {description && (
        <p className="max-w-md text-sm text-muted">{description}</p>
      )}
      <div className="flex gap-2">
        {primaryAction && (
          <button onClick={primaryAction.onClick} className="btn-primary">
            {primaryAction.label}
          </button>
        )}
        {secondaryAction && (
          <a href={secondaryAction.href} className="btn-ghost">
            {secondaryAction.label}
          </a>
        )}
      </div>
    </section>
  );
}

The variant prop is doing real work here โ€” it lets you style differently, log analytics per variant, and enforce that engineers pick a category instead of typing "No data" into a generic component.

The preview slot is where the magic happens

For first-run especially, don't pass an SVG illustration. Pass a ghosted version of the real UI: a faded list row, a sample card, a dimmed chart. This does two things at once โ€” it teaches the user what a populated screen looks like, and it removes the cognitive gap between "empty screen" and "filled screen" that generic illustrations create.

Motion, but restrained

Empty states are a tempting place to over-animate. Resist it. A short fade-in on mount (roughly 150โ€“250ms) is enough. What matters more is the transition out โ€” when the user takes the primary action and data arrives, the empty state should exit before the new content lands, not cross-fade with it.

A rough ratio we use: exit duration around 0.6ร— of entry duration. Fast out, slightly slower in. And honor prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {
  .empty-state { animation: none; transition: none; }
}

Accessibility: the part everyone skips

Three things, non-negotiable:

  1. Announce the state. Use role="status" with aria-live="polite" so screen readers announce the empty message when it appears after a filter or search. Without this, a blind user runs a search and gets silence.
  2. Keep focus reachable. If the empty state replaces a list that had focus, move focus to the primary action or the empty state heading. Don't leave focus on a DOM node that no longer exists.
  3. Contrast the muted text. "Muted" descriptions are the most common WCAG failure we see in empty states. If your body text is #9CA3AF on white, you're at roughly 2.8:1 โ€” below the 4.5:1 minimum. Bump it.

Copywriting that respects the reader

A quick heuristic: if your empty state copy would sound weird coming from a competent human coworker, rewrite it.

  • โŒ "Uh oh! It looks like there's nothing here yet! ๐Ÿ™ˆ"
  • โœ… "No invoices match this filter. Clear filters or create one."

Humour is fine once. Across twelve empty states in one product, it turns into noise. Prefer specificity over personality. "No invoices" beats "Nothing to see here" every time, because it tells the user what kind of thing is missing.

Instrumentation: measure the surface

Empty states are one of the few UI surfaces where analytics are genuinely simple and genuinely valuable. Log at minimum:

  • Which variant rendered (first-run, cleared, no-results).
  • Which action the user took, if any (primary, secondary, or bounced).
  • Time-to-first-action for first-run variants.

Once you have this, you'll find that one or two empty states in your product are silently killing activation. Fix those first.

Where we'd start

If you inherited a product tomorrow and had one week to improve empty states, do this in order:

  1. List every empty state in the product. There are more than you think โ€” usually 10 to 30 in a mid-sized SaaS.
  2. Classify each one as first-run, cleared, no-results, or error-adjacent.
  3. Fix the first-run screens first. That's where the activation money is.
  4. Replace generic illustrations with ghosted previews of the real UI.
  5. Add role="status" and check contrast on muted text.
  6. Instrument the top three by traffic and revisit in a month.

Empty states aren't decoration. They're the moments where your product either explains itself or doesn't. Treat them like the product surface they are, and the rest of the UX starts looking more honest too. If you want a hand auditing yours, that's the kind of work our team does on design and engineering engagements.

#UX#Design Systems#Accessibility#Frontend#Product Design

Want a team like ours?

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

Start a project