Overview
The Empty State component appears when there is no content to display within a section, page, or container. Far from being a dead end, a well-designed empty state is a guidance moment — it explains why nothing is here and what the user can do about it.
Empty states occur in three distinct contexts, each requiring different messaging:
- First-use empty state: The user has just signed up or entered a new section for the first time. No data exists yet because they haven't created any. This is an onboarding opportunity.
- No-results empty state: The user searched or filtered but nothing matched. The system has data, but none fits the current criteria.
- Error empty state: Data should exist but failed to load. This is a system failure, not a user action problem.
When to use an Empty State:
- Lists, tables, or grids with zero items
- Search results pages with no matches
- Dashboard sections awaiting initial data
- Filtered views that exclude all items
- Inbox or notification centers with nothing unread
When NOT to use an Empty State:
- For loading states — use a Skeleton or Spinner instead
- For error messages that affect the whole page — use an error page template
- For confirmations after clearing data — use a Toast or Alert
The key to effective empty states is actionability. An illustration with "Nothing here" is decoration. An illustration with "No projects yet — Create your first project" is a conversion funnel. Always include a call-to-action via a Button. Verify your empty state text meets contrast requirements using the Contrast Checker.
Variants
Empty State Variants
| Variant | Context | Key Elements |
|---|---|---|
| First Use | New user, no data created yet. | Welcoming illustration, headline ("Create your first project"), primary CTA button, optional secondary link to docs/tutorial. |
| No Results | Search or filter returned nothing. | Illustration (magnifying glass, empty box), "No results for [query]", suggestion to adjust filters, clear-filters button. |
| Error / Failed Load | Data exists but couldn't be fetched. | Error illustration, "Something went wrong", retry button, optional support link. |
| Cleared / Completed | User intentionally emptied a list (e.g., inbox zero). | Positive illustration (checkmark, celebration), affirming message ("All caught up!"), no primary CTA needed. |
| Permission Denied | User doesn't have access to this content. | Lock illustration, "You don't have access", request-access button or admin contact info. |
| Compact | Inline empty state within a small container (sidebar panel, card). | No illustration. Icon + single line of text + optional link. |
Layout Variants
| Layout | Description | Use Case |
|---|---|---|
| Centered | Content vertically and horizontally centered in the container. | Full-page or large-section empty states. |
| Top-aligned | Content aligned to the top of the container. | Lists or tables where the empty state replaces row content. |
| Inline | Single line within the content flow. | Sidebar panels, dropdown menus, small cards. |
Properties
Empty State Properties
| Property | Type | Default | Description |
|---|---|---|---|
title | string | — | Headline text (e.g., "No projects yet") |
description | string | — | Supporting text explaining why and what to do |
illustration | ReactNode | — | SVG illustration or image |
icon | ReactNode | — | Compact alternative to illustration |
primaryAction | { label: string; onClick: () => void } | — | Primary CTA button |
secondaryAction | { label: string; onClick: () => void } | — | Secondary text link or ghost button |
size | 'sm' | 'md' | 'lg' | 'md' | Controls illustration size, text sizes, and spacing |
layout | 'centered' | 'top' | 'inline' | 'centered' | Content alignment strategy |
className | string | — | Additional CSS classes |
Important: The title should be concise (5–8 words) and action-oriented when possible. "No projects yet" is better than "This section is empty" because it implies the user can create one. The description should provide context and guidance without being verbose.
Token Mappings
Design Token Mappings
| Token Category | Token Example | Empty State Usage |
|---|---|---|
| Color – Text | --color-text-primary | Title text |
| Color – Text Muted | --color-text-tertiary | Description text |
| Color – Illustration | --color-neutral-300 | Monochrome illustration fill |
| Color – Accent | --color-primary-500 | Illustration accent highlights |
| Color – Surface | --color-surface-default | Container background |
| Typography | --font-size-lg, --font-weight-semibold | Title styling |
| Typography | --font-size-sm, --font-weight-regular | Description styling |
| Spacing | --space-6, --space-8 | Vertical gap between illustration, text, and CTA |
| Spacing | --space-4 | Gap between title and description |
| Max Width | --measure-narrow (400px) | Text block max-width for readability |
Illustrations should use design tokens for fill colors so they adapt to light/dark mode. Avoid hardcoded colors in SVGs — use currentColor or CSS custom properties.
States
Empty State States
| State | Description | Visual Treatment |
|---|---|---|
| Default | Static display with no interaction. | Illustration + text + CTA rendered normally. |
| With Loading CTA | User clicked the action button. | CTA button enters loading state (spinner). Illustration and text remain static. |
| Transitioning Out | Data has been created/loaded, empty state is being replaced. | Fade out with a gentle opacity transition (200–300ms). The new content fades in simultaneously. |
| Animated Illustration | Illustration has subtle motion (floating, pulsing). | Gentle CSS animation. Must respect prefers-reduced-motion — disable all motion or use static fallback. |
Transition note: When an empty state transitions to populated content (e.g., user creates their first item), avoid a jarring jump. Fade the empty state out and the first item in. A 200ms opacity transition is sufficient.
Accessibility
Accessibility Requirements
Semantic Structure:
- Use heading elements (
<h2>or<h3>, matching the page hierarchy) for the empty state title. Do not use<p>with bold styling — screen readers won't identify it as a heading. - Illustrations should be decorative (
aria-hidden="true",role="presentation"). They don't convey information that isn't also in the text. - The container can use
role="status"if the empty state appears dynamically (e.g., after filtering removes all results), triggering a polite screen reader announcement.
WCAG Compliance:
- SC 1.4.3 (Contrast – Minimum): Title text requires 4.5:1 contrast. Description text (often in muted colors) must also meet this threshold — verify with Contrast Checker. Muted gray (#999) on white (#fff) is only 2.85:1 and fails.
- SC 1.4.11 (Non-text Contrast): Illustration elements meaningful to understanding (rare, since they're decorative) need 3:1 contrast.
- SC 2.4.6 (Headings and Labels): The title should be descriptive — "No results found" is better than "Oops!" for screen reader users scanning headings.
- SC 2.4.7 (Focus Visible): The CTA button must have a visible focus indicator. This is inherited from your Button component.
- SC 1.3.1 (Info and Relationships): The heading level must reflect the document hierarchy. An empty state inside a section with an
<h2>should use<h3>.
Dynamic Empty States:
When an empty state appears after user action (clearing filters, deleting all items), announce the change to assistive technology. Use aria-live="polite" on the container or programmatically move focus to the empty state title.
Usage Guidelines
Usage Guidelines
Do:
- Always include a primary call-to-action. An empty state without an action is a dead end.
- Match the tone to the context: welcoming for first-use, helpful for no-results, apologetic for errors.
- Use illustrations that are lightweight (SVG preferred) and adapt to dark mode via design tokens.
- Show the empty state in the same container where content would appear. Don't redirect to a separate page.
- For "no results" states, suggest helpful alternatives: "Try adjusting your filters", "Check your spelling", or show a clear-all-filters button.
Don't:
- Don't use generic messages like "Nothing here" without guidance on what to do next.
- Don't use heavy raster images (PNG/JPG) for illustrations — SVGs scale better and load faster.
- Don't hide the empty state behind a loading spinner indefinitely. If data fetch completes with zero results, show the empty state immediately.
- Don't blame the user. "No results for your search" is fine; "You searched wrong" is hostile.
- Don't overdesign the empty state to the point where it's more visually prominent than populated content. The goal is to guide users away from the empty state by encouraging content creation.
- Don't use the same illustration for every empty state. Context-specific illustrations (empty inbox vs. empty project list) significantly improve comprehension.
Code Snippets
<!-- First-Use Empty State -->
<section class="empty-state" role="status">
<div class="empty-state-illustration" aria-hidden="true">
<svg viewBox="0 0 200 200"><!-- illustration SVG --></svg>
</div>
<h3 class="empty-state-title">Create your first project</h3>
<p class="empty-state-description">
Projects help you organize your work. Start by creating one and inviting your team.
</p>
<div class="empty-state-actions">
<button class="btn btn-primary">New Project</button>
<a href="/docs/getting-started" class="btn btn-ghost">Learn more</a>
</div>
</section>
<!-- No Results Empty State -->
<section class="empty-state empty-state--compact" role="status" aria-live="polite">
<svg class="empty-state-icon" aria-hidden="true"><!-- search icon --></svg>
<h3 class="empty-state-title">No results for "quarterly report"</h3>
<p class="empty-state-description">
Try adjusting your search terms or clearing filters.
</p>
<button class="btn btn-secondary" onclick="clearFilters()">Clear filters</button>
</section>
<!-- Error Empty State -->
<section class="empty-state" role="alert">
<div class="empty-state-illustration" aria-hidden="true">
<svg viewBox="0 0 200 200"><!-- error illustration --></svg>
</div>
<h3 class="empty-state-title">Something went wrong</h3>
<p class="empty-state-description">
We couldn't load your data. Please try again or contact support.
</p>
<div class="empty-state-actions">
<button class="btn btn-primary" onclick="retry()">Try again</button>
<a href="/support" class="btn btn-ghost">Contact support</a>
</div>
</section>// Empty State Component
interface EmptyStateAction {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'ghost';
}
interface EmptyStateProps {
title: string;
description?: string;
illustration?: React.ReactNode;
icon?: React.ReactNode;
primaryAction?: EmptyStateAction;
secondaryAction?: EmptyStateAction;
size?: 'sm' | 'md' | 'lg';
layout?: 'centered' | 'top' | 'inline';
role?: 'status' | 'alert';
}
function EmptyState({
title,
description,
illustration,
icon,
primaryAction,
secondaryAction,
size = 'md',
layout = 'centered',
role = 'status',
}: EmptyStateProps) {
return (
<section
className={`empty-state empty-state--${size} empty-state--${layout}`}
role={role}
>
{illustration && (
<div className="empty-state-illustration" aria-hidden="true">
{illustration}
</div>
)}
{icon && !illustration && (
<div className="empty-state-icon" aria-hidden="true">{icon}</div>
)}
<h3 className="empty-state-title">{title}</h3>
{description && (
<p className="empty-state-description">{description}</p>
)}
{(primaryAction || secondaryAction) && (
<div className="empty-state-actions">
{primaryAction && (
<Button variant={primaryAction.variant ?? 'primary'}
onClick={primaryAction.onClick}>
{primaryAction.label}
</Button>
)}
{secondaryAction && (
<Button variant={secondaryAction.variant ?? 'ghost'}
onClick={secondaryAction.onClick}>
{secondaryAction.label}
</Button>
)}
</div>
)}
</section>
);
}
// Usage: First-use
<EmptyState
title="Create your first project"
description="Projects help you organize your work. Start by creating one."
illustration={<ProjectsIllustration />}
primaryAction={{ label: 'New Project', onClick: () => openCreateDialog() }}
secondaryAction={{ label: 'Learn more', onClick: () => navigate('/docs') }}
/>
// Usage: No results
<EmptyState
title={`No results for "${query}"`}
description="Try adjusting your search terms or clearing filters."
icon={<SearchIcon />}
size="sm"
primaryAction={{ label: 'Clear filters', onClick: clearFilters, variant: 'secondary' }}
/>Design Systems
Material Design 3 does not define a formal "Empty State" component, but provides extensive guidance on "empty conditions" in its content design documentation. MUI developers typically compose empty states from Box, Typography, Button, and custom SVG illustrations. The Material guideline recommends using the product's brand illustration style and keeping messaging instructional rather than whimsical.
Ant Design provides an Empty component with description (text or ReactNode), image (defaults to a built-in SVG), imageStyle, and a children slot for action buttons. Ant offers two built-in illustrations: Empty.PRESENTED_IMAGE_DEFAULT (detailed) and Empty.PRESENTED_IMAGE_SIMPLE (minimal outline). The Empty.PRESENTED_IMAGE_SIMPLE variant is preferred for inline contexts like Select dropdowns. Ant also supports Empty.useEmpty() to globally configure the empty state across all Ant components.
Chakra UI does not include a dedicated Empty State component. Developers compose one using VStack, Heading, Text, Image, and Button. Community templates exist but there's no official primitive.
Radix UI and Headless UI do not provide Empty State components, as they are purely presentational with no interaction patterns requiring headless abstraction.
Atlassian Design System provides EmptyState with header, description, primaryAction, secondaryAction, tertiaryAction, imageUrl, imageWidth, imageHeight, and maxImageWidth. Atlassian's three-tier action hierarchy (primary button, secondary button, tertiary link) is a thoughtful pattern worth adopting. Their guidelines mandate that every empty state must have at least one action — no dead ends.
Polaris (Shopify) provides EmptyState with heading, image, action, secondaryAction, fullWidth, and children. Polaris includes built-in empty state illustrations matching Shopify's brand. Their documentation emphasizes tone: first-use states should be "encouraging", error states should be "reassuring", and no-results states should be "helpful".