Siteplane UI

Empty State

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

Markdown
No projects yet
Create your first project to start organizing work with your team.
"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

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

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Usage

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

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

Composition

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 projects found
Your search for “Onboarding” did not match any projects. Try another search or create a new project.
"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.

No members yet
Mara, Ben and Sofia already work in this workspace. Invite them to collaborate on this project.
"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

Component properties
PropTypeDefaultDescription
EmptyMedia.variantdefault | icondefaultTransparent media wrapper, or a compact icon presentation.
EmptyTitle.level1 | 2 | 3 | 4 | 5 | 6unsetHeading 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.

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.