Empty
Explains why there's nothing to show and what to do next: on first use, with no results, after clearing work, or without access.
import { FolderPlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
export default function EmptyDemo() {
return (
<Empty className="max-w-sm p-4 md:p-6">
<EmptyHeader>
<EmptyMedia variant="icon">
<FolderPlusIcon />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>
Deploy your first app in about a minute.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">Create project</Button>
</EmptyContent>
</Empty>
)
}Anatomy
- 1ContainerA centered stack that stands in for missing content. The outline variant adds a dashed border inside pages.
- 2ActionsOne clear next step — usually the action that creates the first item.
- 3MediaAn icon in a raised tile, or an illustration, that sets the context.
- 4DescriptionWhy it's empty and what to do about it, in a sentence.
- 5TitleSays what's missing, in plain words.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/empty.jsonUsage
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<FolderPlusIcon />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>Deploy your first app in about a minute.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Create project</Button>
</EmptyContent>
</Empty>Examples
Outline
variant="outline" draws a dashed boundary — the convention for an area that is waiting for content, like a drop zone or an empty slot in a layout.
import { UploadCloudIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
export default function EmptyOutline() {
return (
<Empty variant="outline" className="max-w-lg">
<EmptyHeader>
<EmptyMedia variant="icon">
<UploadCloudIcon />
</EmptyMedia>
<EmptyTitle>Upload your first dataset</EmptyTitle>
<EmptyDescription>
CSV, Parquet or JSON up to 2 GB. Columns are detected automatically.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
Choose files
</Button>
</EmptyContent>
</Empty>
)
}No results
When a search or filter returns nothing, repeat the query, suggest why, and offer ways out that don't lose the person's work.
import { SearchIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
export default function EmptyNoResults() {
return (
<Empty className="max-w-md">
<EmptyHeader>
<EmptyMedia variant="icon">
<SearchIcon />
</EmptyMedia>
<EmptyTitle>No results for “refund policy”</EmptyTitle>
<EmptyDescription>
Check the spelling, search for fewer words, or remove the “Last 7
days” filter.
</EmptyDescription>
</EmptyHeader>
<EmptyContent className="flex-row justify-center gap-2">
<Button variant="outline" size="sm">
Remove filter
</Button>
<Button variant="ghost" size="sm">
Clear search
</Button>
</EmptyContent>
</Empty>
)
}All caught up
An empty state can be good news. Say so, and don't invent a call to action when there's nothing to do.
import { CircleCheckIcon } from "lucide-react"
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
export default function EmptyCleared() {
return (
<Empty className="max-w-sm">
<EmptyHeader>
<EmptyMedia variant="icon" className="text-success-11">
<CircleCheckIcon />
</EmptyMedia>
<EmptyTitle>You’re all caught up</EmptyTitle>
<EmptyDescription>
No reviews are waiting on you. New requests will appear here.
</EmptyDescription>
</EmptyHeader>
</Empty>
)
}Restricted
When content exists but the person can't see it, say whose permission they need and how to ask for it.
import { LockIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
export default function EmptyRestricted() {
return (
<Empty className="max-w-sm">
<EmptyHeader>
<EmptyMedia variant="icon">
<LockIcon />
</EmptyMedia>
<EmptyTitle>You don’t have access to Billing</EmptyTitle>
<EmptyDescription>
Only workspace owners can view invoices. Ask Maya Chen for access.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm" variant="outline">
Request access
</Button>
</EmptyContent>
</Empty>
)
}In a card
Inside a card or a table, the empty state takes the place of the content — the surrounding title and actions stay, so the context is never lost.
import { PlugIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
export default function EmptyInCard() {
return (
<Card className="w-full max-w-lg">
<CardHeader>
<CardTitle>Integrations</CardTitle>
<CardDescription>Sync deploy events with your tools.</CardDescription>
<CardAction>
<Button size="sm" variant="outline">
Browse
</Button>
</CardAction>
</CardHeader>
<CardContent>
<Empty variant="outline" className="p-6 md:p-8">
<EmptyHeader>
<EmptyMedia variant="icon">
<PlugIcon />
</EmptyMedia>
<EmptyTitle>No integrations connected</EmptyTitle>
<EmptyDescription>
Connect Slack or Linear to post deploy updates where your team
works.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">Connect Slack</Button>
</EmptyContent>
</Empty>
</CardContent>
</Card>
)
}For tables, see the empty table example.
Guidelines
When to use
- First use. A feature has no data yet. Explain the value and offer the first step.
- No results. A search or filter matched nothing.
- Cleared. Everything is done — an inbox at zero, a finished review queue.
- Restricted. Content exists but the person lacks permission.
When not to use
- For errors that stop a task — use an Alert with a recovery action.
- While data is loading — use a Skeleton in the shape of the content. Never flash an empty state before data arrives.
Writing
- Title — state the situation in plain words: No projects yet, No results for “refund policy”. Avoid blame and exclamation marks.
- Description — one or two sentences on why it's empty and what happens next.
- Actions — one primary action that fills the space. Add a secondary one only when there's a real alternative.
Imagery
Use an icon in EmptyMedia variant="icon" — it names the object that's missing. Save illustrations for first-use states on marketing-adjacent surfaces; in dense product UI they compete with the content around them.
Accessibility
- It's content, not an alert. An empty state is regular content — don't give it
role="alert". When it appears in response to a search, announce the result count in a polite live region instead ("0 results"). - Titles aren't headings by default.
EmptyTitlerenders a<div>. When the empty state is a whole page or section, put a heading of the right level inside it. - Icons are decorative. The title carries the meaning; icons in
EmptyMediadon't need labels. - Focus. After clearing a filter from the empty state, move focus to the results or the search field so keyboard users aren't stranded.
API reference
Empty
A centered, padded container. Accepts all div props.
EmptyMedia
EmptyHeader
Groups the media, title and description, and limits their width for comfortable reading.
EmptyTitle
The situation, set in heading-sm.
EmptyDescription
One or two sentences in muted-foreground. Links inside it are styled.
EmptyContent
Actions and supporting controls under the header.