# Empty

> Explain why a section is empty and help people take the next useful action.

Source: https://www.honestui.com/docs/components/empty

```tsx
import { Book as BookIcon, Route as RouteIcon } from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/honest-ui/ui/empty"

export function EmptyDemo() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <RouteIcon />
        </EmptyMedia>
        <EmptyTitle>No upcoming meetings</EmptyTitle>
        <EmptyDescription>
          Create a meeting to get started.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <div className="flex gap-2">
          <Button size="sm">Create meeting</Button>
          <Button variant="secondary" size="sm">
            <BookIcon className="opacity-72" />
            View docs
          </Button>
        </div>
      </EmptyContent>
    </Empty>
  )
}

```

## Overview [#overview]

Use Empty when a section has nothing to show yet: a dashboard before its first project, an inbox at zero, search results that matched nothing, an upload queue waiting for files. An empty state is a conversation, not a dead end — it should say what is missing, why, and what to do next if there is anything to do.

## Anatomy [#anatomy]

An empty state can include media, a title, a description, and content such as actions or a supporting input. `EmptyHeader` centers the media, title, and description as one block; `EmptyContent` holds whatever comes next. Omit regions you don't need — some states need no action (`Inbox zero`) and some need no illustration.

The container renders a rounded dashed outline by default, which suits drop targets and placeholders; add `border` alongside it for full dashed borders, as the examples do, or remove both for a plain centered layout.

## Usage guidance [#usage-guidance]

Write the title for the real cause. "No projects yet" tells people the space works and awaits input; "Nothing here" could mean a bug, a filter, or an outage. Match the action to the cause: create when the space is new, clear filters when a query came up empty, retry when data failed to arrive, and offer no button when there is genuinely nothing to do — celebrating an empty inbox beats inventing work for it.

Never show an empty state while content is still loading (use [Skeleton](/docs/components/skeleton)) or when a request failed (show the error and a retry instead). Each of those three situations needs its own message; collapsing them into one "No data" state hides whether the problem will resolve on its own.

## Accessibility [#accessibility]

`EmptyTitle` renders a styled `div`, so put a real heading level inside it for people navigating by headings — the same pattern as [Card](/docs/components/card). Keep the description as real text; do not bake the explanation into the illustration.

Treat `EmptyMedia` as decoration: pass `aria-hidden="true"` on decorative icons so screen readers skip straight to the title. The component's stacked backdrop tiles are already hidden from assistive technology. Actions inside `EmptyContent` are ordinary buttons and links — give them specific labels ("Reset filters", not just "OK") because they may be the only controls announced in the region.

Colors come from theme tokens, and the layout reflows cleanly at high zoom; the centered column keeps reading order natural for screen readers top to bottom.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;empty&#x22;]" />
  

  
    
      
        Copy and paste the following code into your project.
      

      ### components/ui/empty.tsx

```tsx
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

function Empty({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty"
      className={cn(
        "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-xl border-dashed p-6 text-center text-balance md:p-12",
        className
      )}
      {...props}
    />
  )
}

function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty-header"
      className={cn(
        "flex max-w-sm flex-col items-center text-center",
        className
      )}
      {...props}
    />
  )
}

const emptyMediaVariants = cva(
  "flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        icon: "relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-card text-foreground shadow-sm shadow-black/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_svg:not([class*='size-'])]:size-4.5",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function EmptyMedia({
  className,
  variant = "default",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
  return (
    <div
      data-slot="empty-media"
      data-variant={variant}
      className={cn("relative mb-6", className)}
      {...props}
    >
      {variant === "icon" && (
        <>
          <div
            className={cn(
              emptyMediaVariants({ variant, className }),
              "pointer-events-none absolute bottom-px origin-bottom-left -translate-x-0.5 scale-84 -rotate-10 shadow-none"
            )}
            aria-hidden="true"
          />
          <div
            className={cn(
              emptyMediaVariants({ variant, className }),
              "pointer-events-none absolute bottom-px origin-bottom-right translate-x-0.5 scale-84 rotate-10 shadow-none"
            )}
            aria-hidden="true"
          />
        </>
      )}
      <div
        className={cn(emptyMediaVariants({ variant, className }))}
        {...props}
      />
    </div>
  )
}

function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty-title"
      className={cn("font-heading text-xl leading-none", className)}
      {...props}
    />
  )
}

function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <div
      data-slot="empty-description"
      className={cn(
        "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary [[data-slot=empty-title]+&]:mt-1",
        className
      )}
      {...props}
    />
  )
}

function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="empty-content"
      className={cn(
        "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
        className
      )}
      {...props}
    />
  )
}

export {
  Empty,
  EmptyHeader,
  EmptyTitle,
  EmptyDescription,
  EmptyContent,
  EmptyMedia,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";
```

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon">
      <Icon />
    </EmptyMedia>
    <EmptyTitle>No projects yet</EmptyTitle>
    <EmptyDescription>Create a project to organize your work.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Create project</Button>
  </EmptyContent>
</Empty>
```

## Don't do this [#dont-do-this]

### Dead ends [#dead-ends]

```tsx
// Bad
<Empty>
  <EmptyHeader>
    <EmptyTitle>Nothing here</EmptyTitle>
  </EmptyHeader>
</Empty>
```

```tsx
// Good
<Empty className="border">
  <EmptyHeader>
    <EmptyMedia variant="icon"><SearchXIcon aria-hidden="true" /></EmptyMedia>
    <EmptyTitle><h2>No matching filters</h2></EmptyTitle>
    <EmptyDescription>Try clearing a status or date filter.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button variant="secondary" size="sm">Reset filters</Button>
  </EmptyContent>
</Empty>
```

"Nothing here" leaves every question open: is this a bug, my filters, my permissions, or an empty database? And with no action offered, the only way out is the back button. Name the cause and hand over the next step whenever one exists.

### Blaming the user [#blaming-the-user]

```tsx
// Bad
<EmptyTitle>You broke it!</EmptyTitle>
<EmptyDescription>This page crashed because of your filters.</EmptyDescription>
```

```tsx
// Good
<EmptyTitle><h2>No results</h2></EmptyTitle>
<EmptyDescription>No transactions match these filters right now.</EmptyDescription>
```

Accusatory copy turns a routine state into an indictment — and it's usually inaccurate, since empty states most often reflect normal circumstances, not user error. Describe the situation neutrally and keep the door open: "no results *yet*", "right now", "with these filters".

### Meaning carried by the picture alone [#meaning-carried-by-the-picture-alone]

```tsx
// Bad
<EmptyHeader>
  <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
</EmptyHeader>
```

```tsx
// Good
<EmptyHeader>
  <EmptyMedia variant="icon"><FolderIcon aria-hidden="true" /></EmptyMedia>
  <EmptyTitle><h2>No projects yet</h2></EmptyTitle>
  <EmptyDescription>Create your first project to get started.</EmptyDescription>
</EmptyHeader>
```

An unexplained folder icon says something different to everyone and nothing to screen readers. The icon sets tone; the title and description carry the actual message. Mark the media decorative and make the text self-sufficient.

## Examples [#examples]

Match the message and recovery action to the reason the content is empty.

### No matching results [#no-matching-results]

A filter problem with a one-click way back.

```tsx
import { SearchX as SearchXIcon } from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/honest-ui/ui/empty"

export function EmptyFilteredResults() {
  return (
    <Empty className="border">
      <EmptyHeader>
        <EmptyMedia variant="icon"><SearchXIcon /></EmptyMedia>
        <EmptyTitle>No matching filters</EmptyTitle>
        <EmptyDescription>Try clearing a status or date filter.</EmptyDescription>
      </EmptyHeader>
      <EmptyContent><Button variant="secondary" size="sm">Reset filters</Button></EmptyContent>
    </Empty>
  )
}

```

### Empty inbox [#empty-inbox]

A genuine achievement with no action attached.

```tsx
import { Inbox as InboxIcon } from "honestui/icons"

import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/honest-ui/ui/empty"

export function EmptyInboxZero() {
  return (
    <Empty className="border">
      <EmptyHeader>
        <EmptyMedia variant="icon"><InboxIcon /></EmptyMedia>
        <EmptyTitle>Inbox zero</EmptyTitle>
        <EmptyDescription>No unread notifications are waiting for you.</EmptyDescription>
      </EmptyHeader>
    </Empty>
  )
}

```

### Upload queue [#upload-queue]

A drop-target state inviting the next file.

```tsx
import { CloudUpload as UploadCloudIcon } from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/honest-ui/ui/empty"

export function EmptyUploadQueue() {
  return (
    <Empty className="border-dashed border">
      <EmptyHeader>
        <EmptyMedia variant="icon"><UploadCloudIcon /></EmptyMedia>
        <EmptyTitle>No files queued</EmptyTitle>
        <EmptyDescription>Drop assets here or browse from your computer.</EmptyDescription>
      </EmptyHeader>
      <EmptyContent><Button size="sm">Upload files</Button></EmptyContent>
    </Empty>
  )
}

```

## API reference [#api-reference]

All parts accept native `div` props plus the following:

| Part               | Prop      | Values            | Default   |
| ------------------ | --------- | ----------------- | --------- |
| `Empty`            | —         | —                 | —         |
| `EmptyHeader`      | —         | —                 | —         |
| `EmptyMedia`       | `variant` | `default`, `icon` | `default` |
| `EmptyTitle`       | —         | —                 | —         |
| `EmptyDescription` | —         | —                 | —         |
| `EmptyContent`     | —         | —                 | —         |

`Empty` provides the centered, dashed-border container. `EmptyHeader` wraps media, title, and description with constrained width and centering. With `variant="icon"`, `EmptyMedia` draws a bordered icon tile flanked by two rotated, `aria-hidden` backdrop tiles. `EmptyTitle` and `EmptyDescription` provide styling only — supply your own heading semantics inside the title. `EmptyContent` constrains actions to a readable measure.
