Loading…
Loading…
Displayed when there is no content to show, guiding users toward taking action.
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:
When to use an Empty State:
When NOT to use an Empty State:
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.
| 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 | 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. |
| 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 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.
| 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.
Semantic Structure:
<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.aria-hidden="true", role="presentation"). They don't convey information that isn't also in the text.role="status" if the empty state appears dynamically (e.g., after filtering removes all results), triggering a polite screen reader announcement.WCAG Compliance:
<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.
Do:
Don't:
<!-- 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' }}
/>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".