Empty states: first-use, cleared, filtered-empty and error variants
Never leave a region blank: distinguish first-use (explain + create action), user-cleared (confirm success), filtered/no-results (show criteria + clear), and error (what failed + retry) empty states, each with one primary action.
Overview
What: What a list, panel or page shows when it has no content to display.
Why: Blank areas make users wonder whether content is loading, broken or filtered. Empty states are also the most natural teaching moment: they appear exactly when the user is looking at a feature they haven't used yet.
Use when: Every list, table, search result, dashboard widget and detail panel that can have zero items.
Do not use when: Content is still loading — show a loading state instead. Never flash an empty state before data arrives; that falsely tells users they have nothing.
Implementation
- Pick the variant:
| Variant | Trigger | Content | Primary action |
|---|---|---|---|
| First use | Feature never used | What this area holds and why it matters (1–2 sentences) | Create/connect ("Create a project") |
| User-cleared | All items completed/archived | Positive confirmation ("No pending proposals") | None, or a link to history |
| No results | Search/filter matches nothing | Echo the query/filters; suggestions | "Clear filters" / edit search |
| Error | Load failed | What failed, whether data is safe | "Try again" + status/help link |
| No permission | User lacks access | Who can grant access | "Request access" or owner contact |
- Structure (Blankslate-style): optional small illustration or icon (neutral alert icon for errors), heading (≤8 words), one or two sentences, one primary button, optional secondary text link ("Learn how projects work").
- Don't disguise failures as emptiness: if the fetch failed, render the error variant, not "No items yet".
- Size: in a full page, center within the content column with max-width ~480px; in a small widget, use a compact inline version (icon + sentence + link).
- Offer sample content when learning is hard without data ("Explore with the sample project"), labeled as sample and removable.
- Write in the product's vocabulary: name the object and its value, not the mechanism.
- Heading level fits the page outline; icons are
aria-hidden.
Make the empty case a type error, not a silent gap. Give every list/table view a required status so a call site cannot forget to design the empty case:
type EmptyStateProps = { status: "no-data" | "no-results" | "error"; title: string; description?: string;
action?: { label: string; onClick: () => void }; secondaryAction?: { label: string; onClick: () => void } };
function EmptyState({ status, title, description, action, secondaryAction }: EmptyStateProps) {
return (<div role="status" className="flex flex-col items-center gap-3 py-16 text-center">
<StatusIcon status={status} aria-hidden="true" /><h3>{title}</h3>{description && <p>{description}</p>}
<div className="flex gap-2">{action && <Button onClick={action.onClick}>{action.label}</Button>}
{secondaryAction && <Button variant="outline" onClick={secondaryAction.onClick}>{secondaryAction.label}</Button>}</div></div>);
}
// <EmptyState status="no-data" title="No alerts yet" description="Alerts notify you when a metric crosses a threshold you set."
// action={{ label: "Create your first alert", onClick: openDialog }} secondaryAction={{ label: "Load sample alerts", onClick: loadDemo }} />
role="status" tells assistive technology this is informational, not an error dialog. Model the view's data as a discriminated union (idle | loading | error | empty | ready) so the empty branch is exhaustive-checked by the compiler.
Verification
- Each list/table has fixtures for first-use, no-results, and error empty states, and they render differently.
- No-results state names the active query/filters and offers "Clear filters".
- Error state never says "No items" and offers retry.
- Empty state is not shown while loading (throttle network; skeleton appears first).
- Each variant has at most one primary action.
- Illustrations are decorative (
aria-hidden/empty alt) and not required to understand the message. - Every list/table component requires an explicit empty-state status prop or union branch (compile-time check).
Sources:
Limitations
- Illustrations add weight and localization cost; in dense admin tools, a text-only empty state is usually better.
- Sample data can confuse metrics and exports; tag it and exclude it from billing/usage counts.
- For permission cases in multi-tenant apps, revealing that a resource exists may leak information; use a generic not-found where that matters.
- Values verified 2026-09-26: all 2 numeric values in this item were checked (1 in Chromium lab tests, 1 for consistency with house policy) and confirmed; no corrections were needed.