EmptyState
<EmptyState> fills a region that has no content and explains why. Four variants — they’re not aesthetic choices; each names a different reason for emptiness, and each has its own copy and CTA shape.
When to use
- A list, table, or grid with zero rows.
- A dashboard whose data hasn’t been generated yet.
- A region the user can’t see (permission denied).
- A region that failed to load (network error).
Don’t conflate the variants. A permission-blocked empty state with a “Create your first” CTA is cruel. Pick the variant that names the actual reason.
Blank slate
User is new — there’s nothing to show because nothing has happened yet. Full size, with icon + description + primary CTA.
No agents yet
Create your first voice agent to start handling calls.
<EmptyState variant="blank-slate" icon={<IconInbox className="size-10 text-muted-foreground" />} title="No agents yet" description="Create your first voice agent to start handling calls." action={<Button>Create your first agent</Button>} secondaryAction={<Button variant="outline">Import from CSV</Button>}/>Filtered
Data exists, the filters hide it. Compact layout, no icon, single CTA: clear filters.
No agents match these filters
<EmptyState variant="filtered" title="No agents match these filters" action={<Button variant="outline">Clear filters</Button>}/>Permission
The user can’t see this. Lock icon + an “ask for access” CTA. Don’t suggest creating things they aren’t allowed to.
You don't have access to this page
Members management is admin-only. Ask a workspace admin to invite you.
<EmptyState variant="permission" icon={<IconLock className="size-10 text-muted-foreground" />} title="You don't have access to this page" description="Members management is admin-only. Ask a workspace admin to invite you." action={<Button>Request access</Button>}/>Error
Fetch failed. Alert icon + retry CTA. Keep the description short and concrete; defer the trace ID to a separate detail line if needed.
Couldn't load agents
Check your connection and try again.
<EmptyState variant="error" icon={<IconAlertCircle className="size-10 text-destructive" />} title="Couldn't load agents" description="Check your connection and try again." action={<Button>Retry</Button>}/>API
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "blank-slate" | "filtered" | "permission" | "error" | "blank-slate" | Reason this region is empty. Drives layout, icon visibility, and copy expectations. |
title * | string | — | Headline. Lead with the noun, sentence case. |
description | string | — | One-sentence elaboration. Skip in filtered variant. |
icon | ReactNode | string | — | Pass an icon component (preferred) or a single emoji. Hidden in filtered variant. |
action | ReactNode | — | Primary CTA. Pass a <Button>. |
secondaryAction | ReactNode | — | Optional second CTA next to action. |
className | string | — | Forwarded. |
Design guidelines
✓ Do
- Match the variant to the reason: blank-slate (new), filtered (hidden by query), permission (denied), error (failed).
- Lead the title with the noun: "No agents yet", "No results match these filters".
- Make the primary action solve the problem stated. "Clear filters" for filtered. "Retry" for error.
✗ Don't
- Show a "Create your first" CTA on a permission-denied surface.
- Add an icon to a filtered-zero state. The pattern is intentionally compact.
- Stack three CTAs. One primary, optionally one secondary, never more.
Accessibility
- Title renders as
<h2>— slot it into a region that doesn’t already own the page heading. EmptyState inside a<section>is correct; inside a<main>next to the page<h1>is fine. - Color of the error icon is
text-destructive, but the message (“Couldn’t load…”) carries the meaning — color is never the only signal. - Action buttons are real
<Button>s with full keyboard support — don’t pass styled<div>s.