Skip to content

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.

SourceRegistry
No projects yet
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

No projects yet
  1. 1ContainerA centered stack that stands in for missing content. The outline variant adds a dashed border inside pages.
  2. 2ActionsOne clear next step — usually the action that creates the first item.
  3. 3MediaAn icon in a raised tile, or an illustration, that sets the context.
  4. 4DescriptionWhy it's empty and what to do about it, in a sentence.
  5. 5TitleSays what's missing, in plain words.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/empty.json

Usage

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.

Upload your first dataset
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.

No results for “refund policy”
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.

You’re all caught up
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.

You don’t have access to Billing
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.

Integrations
No integrations connected
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.
No invoices yet
DoSpecific title, a helpful next step, one action.
Oops! Nothing here!
Don’tGeneric, emotional copy with no explanation or next step.

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. EmptyTitle renders 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 EmptyMedia don'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.

PropTypeDefault
variant

Outline adds a dashed boundary for areas waiting for content.

"default" | "outline""default"

EmptyMedia

PropTypeDefault
variant

Icon puts the icon in a raised 44px tile. Default leaves the slot unstyled for illustrations.

"default" | "icon""default"

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.