Empty States Are Your Second Homepage: Designing Them Like Product Surface
Empty states get treated like an afterthought — a shrug icon and a sentence. They're actually one of the highest-leverage screens in your product. Here's how to design and ship them like it.

Most teams spend weeks polishing the dashboard that appears once a user has data, and about forty minutes on the screen they'll actually see first. That's backwards. The empty state is the second homepage of your product — the screen that greets every new account, every filtered-to-nothing table, every fresh workspace — and it deserves the same rigor as your marketing site.
This is a practical breakdown of how we design and build empty states at 72Technologies: what the categories are, what the copy should do, how motion fits in, and a component pattern that keeps engineering sane.
The three empty states, and why they're not the same thing
The biggest mistake we see is treating "empty" as a single condition. It isn't. There are at least three distinct flavors, and each one wants a different job done.
- First-run empty — the user has never had data here. This is an onboarding surface.
- User-cleared empty — the user had data and removed it, archived it, or completed it. This is a confirmation and next-action surface.
- Filter/search empty — data exists, but the current query returned nothing. This is a recovery surface.
If you show the same illustration and "Nothing here yet" copy for all three, you're wasting the moment. First-run wants to teach. User-cleared wants to celebrate or reassure. Filter-empty wants to shorten the path back to results.
A quick test
Open your product. Delete everything in one list. Then apply a filter that returns zero rows. If both screens look identical, that's the bug.
What first-run empty states actually need to do
The first-run empty state is an activation funnel disguised as a placeholder. It has one primary job: get the user to the moment where the product starts working for them.
A good first-run empty state usually contains:
- A one-line description of what this space is for. Not a tagline. A functional sentence: "This is where invoices you send will appear."
- A single primary action. One button. Not three. If there are multiple ways to create data (import, connect, manual), pick the most successful one from your funnel data and make the others secondary text links.
- A concrete preview of what "filled" looks like. A ghosted row, a sample card, a faint chart shape. This does more for comprehension than any illustration.
- A trust signal or escape hatch. "Import from CSV" or "See a sample workspace" for the users who don't want to commit yet.
Avoid: mascots doing yoga, three-paragraph explanations, and any copy that starts with "Oops!" The user did not oops. They opened your app.
Filter-empty is a search UX problem
When a user filters or searches to zero results, the empty state is not decorative. It's a recovery mechanism, and the interaction cost of every element matters.
The pattern that consistently works for us:
- Echo the query back. "No results for
unpaidin September 2026`" tells the user the system heard them. - Offer a specific loosening action, not a generic reset. "Search all dates" beats "Clear filters."
- If you have fuzzy matches or related results, show them below the empty message with a clear label. Don't silently rewrite the query.
We've seen conversion on "return to a useful state" jump noticeably when the escape action is specific rather than generic. In one B2B dashboard, replacing a single Clear all filters button with two contextual actions (Show all months, Include archived) cut the rate of users abandoning the filtered view.
Copy is the design
Empty state illustrations get all the Dribbble attention, but the copy carries the weight. A few rules we hold to:
- Name the object. "No projects yet" is better than "Nothing here." It reinforces the mental model.
- Use the verb from the button. If the CTA says Create project, the body copy should say "Create your first project to…" not "Get started by adding one."
- Skip the exclamation marks. They read as nervous.
- Localize with care. "Your inbox is empty" translates cleanly. "Inbox zero achieved!" does not.
One heuristic: if you removed the illustration entirely, would the empty state still do its job? If not, the copy is under-built.
Motion, but with a rule
Empty states are a good place for a small amount of motion — a fade-in, a subtle float on the preview card — because there's no data to distract from. But motion here should follow one rule: it must decay.
A looping animation on an empty state becomes visual noise the third time a user hits that screen. Play it once on mount, then stop. If the user returns within the same session, skip the animation entirely.
Our default ratios:
- Entrance: 200–320ms, ease-out
- Preview card float: one cycle, then rest
- CTA attention pulse: never. If the CTA needs a pulse to be found, the layout is wrong.
And respect prefers-reduced-motion. Always.
A component pattern that scales
Here's the shape we reach for when building empty states as a reusable primitive. It forces the team to think about which of the three flavors they're building, and keeps the API tight.
type EmptyStateVariant = 'first-run' | 'cleared' | 'no-results';
interface EmptyStateProps {
variant: EmptyStateVariant;
title: string;
description?: string;
primaryAction?: {
label: string;
onClick: () => void;
};
secondaryAction?: {
label: string;
onClick: () => void;
};
preview?: React.ReactNode; // ghosted row, sample card, etc.
query?: string; // only meaningful for 'no-results'
}
export function EmptyState({
variant,
title,
description,
primaryAction,
secondaryAction,
preview,
query,
}: EmptyStateProps) {
const prefersReducedMotion = useReducedMotion();
return (
<div
role="status"
aria-live="polite"
className="flex flex-col items-center gap-4 py-16 text-center"
>
{preview && (
<div
className={
prefersReducedMotion
? 'opacity-60'
: 'opacity-60 animate-in fade-in duration-300'
}
aria-hidden="true"
>
{preview}
</div>
)}
<h2 className="text-lg font-semibold">
{variant === 'no-results' && query ? (
<>No results for <span className="font-mono">{query}</span></>
) : (
title
)}
</h2>
{description && (
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
)}
<div className="flex gap-2">
{primaryAction && (
<Button onClick={primaryAction.onClick}>{primaryAction.label}</Button>
)}
{secondaryAction && (
<Button variant="ghost" onClick={secondaryAction.onClick}>
{secondaryAction.label}
</Button>
)}
</div>
</div>
);
}
A few things worth pointing out in this shape:
variantis required. You can't render an empty state without declaring which one it is. That's the point.aria-live="polite"matters for filter-empty states, where the content changes without a navigation. Screen readers should announce that the result set is empty.- The preview slot is a
ReactNode, not a hardcoded illustration. Each feature team owns what "filled" looks like in their surface.
Accessibility notes
Empty states are surprisingly easy to get wrong for assistive tech. A few things to check:
- The heading level should match the section it's replacing. If the list normally sits under an
<h1>, the empty state's title should be an<h2>— not a styled<div>. - If the empty state appears after a user action (deleting the last item, clearing a filter), announce it via a live region.
- Any illustration should be
aria-hidden. It's decoration. - Contrast on ghosted previews still needs to meet 3:1 for non-text UI. "Faint" is not a license to skip contrast.
Measuring whether your empty states work
Empty states are measurable, and most teams don't measure them. A few events worth firing:
- Impressions per variant (first-run vs. cleared vs. no-results)
- CTA click-through per variant
- Time-to-first-action from first-run empty
- Recovery rate on no-results (did the user get to a non-empty state within N seconds?)
Once you have that, empty states stop being a design opinion and start being a funnel you can tune. In our experience, first-run empty states are one of the highest-ROI screens to A/B test — the traffic is guaranteed, the intent is high, and small copy changes move activation numbers more than you'd expect.
Where we'd start
If you're staring at a product with mediocre empty states, don't try to redesign all of them at once. Do this instead:
- Audit your top three most-hit empty states. Screenshot them.
- Classify each one: first-run, cleared, or no-results.
- Rewrite the copy first, before touching any illustration. Give it the verb from the CTA and name the object.
- Add a ghosted preview to the first-run variants.
- Ship, instrument, and come back in two weeks with data.
That's usually enough to turn empty states from a decoration problem into a growth surface. If you want a hand doing this across a larger product, that's the kind of work our design and product team does day-to-day.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Design Tokens That Survive Contact With Engineering: A Naming Convention That Scales
Most design token systems die at the second theme. Here's a three-tier naming convention that survives dark mode, brand refreshes, and the inevitable Tailwind migration.

Skeleton Screens Are Lying to Your Users: A Better Loading Strategy
Skeleton screens became the default for perceived performance, but most implementations make apps feel slower and less trustworthy. Here's when to use them, when to skip them, and what to build instead.

Focus Rings Are a Product Decision: Designing Keyboard Focus That Ships
Most focus rings are either invisible or ugly enough that a designer strips them out. Here's how to design keyboard focus that survives brand review, passes WCAG, and doesn't break your dark mode.
