# Empty State

Explain why a collection has no content and what to do next.

Empty project list with actions

[Complete TSX file](/examples/empty-state/default.tsx)

```tsx
"use client";

import { FolderIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/siteplane/components/empty-state";

export default function Example() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <FolderIcon aria-hidden="true" />
        </EmptyMedia>
        <EmptyTitle>No projects yet</EmptyTitle>
        <EmptyDescription>
          Create your first project to start organizing work with your team.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <div className="flex flex-wrap justify-center gap-2">
          <Button>Create project</Button>
          <Button variant="ghost">Import project</Button>
        </div>
      </EmptyContent>
    </Empty>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/empty-state
```

With npm: `npx shadcn@4.18.0 add @siteplane/empty-state`. [Registry JSON](/registry/empty-state.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/avatar @siteplane/avatar-group @siteplane/button
```

## Usage

```tsx
import {
  Empty,
  EmptyHeader,
  EmptyTitle,
} from "@/siteplane/components/empty-state";

<Empty>
  <EmptyHeader>
    <EmptyTitle>No results</EmptyTitle>
  </EmptyHeader>
</Empty>;
```

## Composition

```text
Empty
├── EmptyHeader
│   ├── EmptyMedia
│   ├── EmptyTitle
│   └── EmptyDescription
└── EmptyContent
```

## No results

When a search or filter matches nothing, say so and offer a way back. Keep the search and filter controls visible so people can adjust them.

No matching projects

[Complete TSX file](/examples/empty-state/no-results.tsx)

```tsx
"use client";

import { PlusIcon, SearchIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/siteplane/components/empty-state";

export default function Example() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <SearchIcon aria-hidden="true" />
        </EmptyMedia>
        <EmptyTitle>No projects found</EmptyTitle>
        <EmptyDescription>
          Your search for “Onboarding” did not match any projects. Try another
          search or create a new project.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <div className="flex flex-wrap justify-center gap-2">
          <Button variant="ghost">Clear search</Button>
          <Button>
            <PlusIcon aria-hidden="true" />
            New project
          </Button>
        </div>
      </EmptyContent>
    </Empty>
  );
}
```

## Avatar media

Use the default EmptyMedia variant for richer media, such as the people who could join a project.

Invite team members

[Complete TSX file](/examples/empty-state/with-avatars.tsx)

```tsx
"use client";

import { PlusIcon } from "lucide-react";
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/siteplane/components/avatar";
import { AvatarGroup } from "@/siteplane/components/avatar-group";
import { Button } from "@/siteplane/components/button";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/siteplane/components/empty-state";

const teammates = [
  { initials: "ME", image: "https://ui.siteplane.io/avatars/mara.webp" },
  { initials: "BC", image: "https://ui.siteplane.io/avatars/ben.webp" },
  { initials: "SN", image: "https://ui.siteplane.io/avatars/sofia.webp" },
];

export default function Example() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia aria-hidden="true">
          <AvatarGroup size="xl">
            {teammates.map((teammate) => (
              <Avatar key={teammate.initials}>
                <AvatarImage alt="" src={teammate.image} />
                <AvatarFallback>{teammate.initials}</AvatarFallback>
              </Avatar>
            ))}
          </AvatarGroup>
        </EmptyMedia>
        <EmptyTitle>No members yet</EmptyTitle>
        <EmptyDescription>
          Mara, Ben and Sofia already work in this workspace. Invite them to
          collaborate on this project.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button>
          <PlusIcon aria-hidden="true" />
          Invite members
        </Button>
      </EmptyContent>
    </Empty>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| EmptyMedia.variant | default \| icon | default | Transparent media wrapper, or a compact icon presentation. |
| EmptyTitle.level | 1 \| 2 \| 3 \| 4 \| 5 \| 6 | unset | Heading level: the title renders as h1 to h6, with the same look. Unset, it is a div. |
`Empty`, `EmptyHeader`, `EmptyMedia`, `EmptyTitle`, `EmptyDescription` and
`EmptyContent` accept native div attributes, refs, children and string
className. Empty provides centered layout and vertical spacing. Header groups
the message; Media precedes Title and Description; Content holds actions or
supplementary content. Title is a div unless `level` makes it a heading. There
are no loading, error, onCreate, size or open props.

[Read the installed source](/source/empty-state).

## Usage notes

Show an empty state after loading has settled. Your application decides whether it means no data yet, no matching results or a recoverable failure, and which action fits. Set `level` on EmptyTitle when the page outline needs the title as a heading, such as `level={2}` for an empty state that stands in for a page section.
