# Empty

> A placeholder for screens with nothing to show yet, with an icon, a message and the next action.

Docs: https://hextaui.com/docs/empty
Markdown: https://hextaui.com/docs/empty.md

```tsx title="components/examples/empty/demo.tsx"
import { IconInbox } from "@tabler/icons-react"

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

export function EmptyDemo() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia variant="stack">
          <IconInbox />
        </EmptyMedia>
        <EmptyTitle>You’re all caught up</EmptyTitle>
        <EmptyDescription>
          New mentions, reviews and replies land here. Nothing needs you right
          now.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Notification settings
        </Button>
      </EmptyContent>
    </Empty>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/empty.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/empty.tsx"
"use client"

import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

function dataAttributes(slot: string, extra?: Record<string, string>) {
  return { "data-slot": slot, ...extra } as Record<string, string>
}

const emptyVariants = cva(
  "group/empty flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-(--empty-gap) rounded-xl p-(--empty-padding) text-center text-balance",
  {
    variants: {
      variant: {
        default:
          "[--empty-media-bg:var(--color-muted)] [--empty-media-ring:0px]",
        outline:
          "border-(length:--hairline) border-dashed border-border [--empty-media-bg:var(--color-muted)] [--empty-media-ring:0px]",
        muted:
          "bg-muted/50 [--empty-media-bg:var(--color-background)] [--empty-media-ring:var(--hairline)]",
      },
      size: {
        default:
          "[--empty-gap:--spacing(6)] [--empty-media-gap:--spacing(2)] [--empty-media-radius:var(--radius-lg)] [--empty-media-size:--spacing(10)] [--empty-padding:--spacing(12)] [--empty-title-size:var(--text-lg)]",
        sm: "[--empty-gap:--spacing(4)] [--empty-media-gap:--spacing(1)] [--empty-media-radius:var(--radius-md)] [--empty-media-size:--spacing(9)] [--empty-padding:--spacing(6)] [--empty-title-size:var(--text-base)]",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type EmptyVariant = NonNullable<VariantProps<typeof emptyVariants>["variant"]>
type EmptySize = NonNullable<VariantProps<typeof emptyVariants>["size"]>

type EmptyProps = useRender.ComponentProps<"div"> & {
  variant?: EmptyVariant
  size?: EmptySize
  animated?: boolean
}

function Empty({
  className,
  variant = "default",
  size = "default",
  animated = true,
  render,
  ...props
}: EmptyProps) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      { className: cn(emptyVariants({ variant, size }), className) },
      props,
      dataAttributes("empty", {
        "data-variant": variant,
        "data-size": size,
        ...(animated ? { "data-animated": "" } : {}),
      })
    ),
  })
}

const enter = "group-data-animated/empty:motion-safe:animate-empty-in"

function EmptyHeader({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "flex w-full max-w-sm min-w-0 flex-col items-center gap-2",
          className
        ),
      },
      props,
      dataAttributes("empty-header")
    ),
  })
}

const emptyMediaVariants = cva(
  "mb-[var(--empty-media-gap,--spacing(2))] flex shrink-0 items-center justify-center [--empty-delay:0ms] group-data-animated/empty:motion-safe:animate-empty-in [&_svg]:pointer-events-none [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        icon: "size-[var(--empty-media-size,--spacing(10))] rounded-[var(--empty-media-radius,var(--radius-lg))] bg-[var(--empty-media-bg,var(--color-muted))] text-foreground ring-[length:var(--empty-media-ring,0px)] ring-foreground/10 [&_svg:not([class*='size-'])]:size-5",
        stack:
          "relative isolate mx-[calc(var(--empty-media-size,--spacing(10))*0.4)] size-[calc(var(--empty-media-size,--spacing(10))*1.2)] rounded-[var(--empty-media-radius,var(--radius-lg))] text-foreground before:absolute before:inset-0 before:translate-x-[-28%] before:translate-y-[6%] before:scale-90 before:-rotate-10 before:rounded-[inherit] before:bg-muted before:ring-(length:--hairline) before:ring-foreground/8 before:transition-[rotate,translate,scale] before:duration-500 before:ease-spring after:absolute after:inset-0 after:translate-x-[28%] after:translate-y-[6%] after:scale-90 after:rotate-10 after:rounded-[inherit] after:bg-muted after:ring-(length:--hairline) after:ring-foreground/8 after:transition-[rotate,translate,scale] after:duration-500 after:ease-spring motion-reduce:before:transition-none motion-reduce:after:transition-none [&_svg:not([class*='size-'])]:size-5 [@media(hover:hover)]:group-hover/empty:before:translate-x-[-42%] [@media(hover:hover)]:group-hover/empty:before:-rotate-16 [@media(hover:hover)]:group-hover/empty:after:translate-x-[42%] [@media(hover:hover)]:group-hover/empty:after:rotate-16",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

type EmptyMediaVariant = NonNullable<
  VariantProps<typeof emptyMediaVariants>["variant"]
>

type EmptyMediaProps = useRender.ComponentProps<"div"> & {
  variant?: EmptyMediaVariant
}

function EmptyMedia({
  className,
  variant = "default",
  render,
  children,
  ...props
}: EmptyMediaProps) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(emptyMediaVariants({ variant }), className),
        ...(variant !== "default" ? { "aria-hidden": true } : {}),
        children:
          variant === "stack" ? (
            <span
              data-slot="empty-icon-tile"
              className="relative z-10 grid size-full place-items-center rounded-[inherit] bg-card ring-(length:--hairline) ring-foreground/10 forced-colors:border"
            >
              {children}
            </span>
          ) : (
            children
          ),
      },
      props,
      dataAttributes("empty-icon", { "data-variant": variant })
    ),
  })
}

function EmptyTitle({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "max-w-full min-w-0 font-heading text-[length:var(--empty-title-size,var(--text-lg))] leading-snug font-medium tracking-tight wrap-anywhere text-foreground [--empty-delay:60ms]",
          enter,
          className
        ),
      },
      props,
      dataAttributes("empty-title")
    ),
  })
}

function EmptyDescription({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          enter,
          "max-w-full min-w-0 text-sm/relaxed wrap-anywhere text-muted-foreground [--empty-delay:110ms] [&_a:not([data-slot])]:text-foreground [&_a:not([data-slot])]:underline [&_a:not([data-slot])]:decoration-foreground/30 [&_a:not([data-slot])]:underline-offset-4 [&_a:not([data-slot])]:transition-colors [&_a:not([data-slot])]:hover:decoration-foreground [&_a:not([data-slot])]:focus-visible:rounded-xs [&_a:not([data-slot])]:focus-visible:ring-3 [&_a:not([data-slot])]:focus-visible:ring-focus-ring [&_a:not([data-slot])]:focus-visible:outline-hidden [&>p:not(:last-child)]:mb-2",
          className
        ),
      },
      props,
      dataAttributes("empty-description")
    ),
  })
}

function EmptyContent({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance [--empty-delay:170ms]",
          enter,
          className
        ),
      },
      props,
      dataAttributes("empty-content")
    ),
  })
}

export {
  Empty,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
  EmptyDescription,
  EmptyContent,
  emptyVariants,
  emptyMediaVariants,
}
export type { EmptyProps, EmptyMediaProps }
```

Update the import paths to match your project setup.

## Usage

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

```tsx
<Empty>
  <EmptyHeader>
    <EmptyMedia variant="stack">
      <IconFolder />
    </EmptyMedia>
    <EmptyTitle>No projects yet</EmptyTitle>
    <EmptyDescription>Create a project to get started.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Create project</Button>
  </EmptyContent>
</Empty>
```

## Composition

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

## Examples

### Entrance

When an empty state replaces content, its parts fade up one after another: media, title, description, then actions. Dismiss the notifications to see it. Under reduced motion it just appears. Set `animated={false}` for empty states that are there from the first paint.

```tsx title="components/examples/empty/transition.tsx"
"use client"

import * as React from "react"
import { IconBellOff, IconX } from "@tabler/icons-react"

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

const initial = [
  "Jun approved your pull request",
  "Mira mentioned you in #design",
  "Deploy to production finished",
]

export function EmptyTransition() {
  const [items, setItems] = React.useState(initial)

  return (
    <div className="flex min-h-64 w-full max-w-sm flex-col">
      {items.length > 0 ? (
        <ul className="flex flex-col gap-1 rounded-xl border p-1.5 text-sm">
          {items.map((item) => (
            <li
              key={item}
              className="flex items-center justify-between gap-2 rounded-md py-1 ps-2.5"
            >
              <span className="min-w-0 truncate">{item}</span>
              <Button
                variant="ghost"
                size="icon-sm"
                aria-label={`Dismiss “${item}”`}
                onClick={() =>
                  setItems(items.filter((other) => other !== item))
                }
              >
                <IconX />
              </Button>
            </li>
          ))}
        </ul>
      ) : (
        <Empty variant="outline" size="sm">
          <EmptyHeader>
            <EmptyMedia variant="stack">
              <IconBellOff />
            </EmptyMedia>
            <EmptyTitle>No notifications</EmptyTitle>
            <EmptyDescription>
              Dismissed items are gone for good.
            </EmptyDescription>
          </EmptyHeader>
          <EmptyContent>
            <Button
              variant="outline"
              size="sm"
              onClick={() => setItems(initial)}
            >
              Bring them back
            </Button>
          </EmptyContent>
        </Empty>
      )}
    </div>
  )
}
```

### Outline

`variant="outline"` draws a dashed hairline border, the usual way to mark a drop zone or an area waiting for content.

```tsx title="components/examples/empty/outline.tsx"
import { IconCloudUpload } from "@tabler/icons-react"

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

export function EmptyOutline() {
  return (
    <Empty variant="outline">
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <IconCloudUpload />
        </EmptyMedia>
        <EmptyTitle>Cloud storage is empty</EmptyTitle>
        <EmptyDescription>
          Upload files to keep them in sync across every device.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Upload files
        </Button>
      </EmptyContent>
    </Empty>
  )
}
```

### Muted

`variant="muted"` sits on a soft fill. The icon tile switches to a raised white surface so it still stands out.

```tsx title="components/examples/empty/muted.tsx"
import { IconBell } from "@tabler/icons-react"

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

export function EmptyMuted() {
  return (
    <Empty variant="muted">
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <IconBell />
        </EmptyMedia>
        <EmptyTitle>No notifications</EmptyTitle>
        <EmptyDescription>
          You’re all caught up. New notifications will appear here.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Notification settings
        </Button>
      </EmptyContent>
    </Empty>
  )
}
```

### Small

`size="sm"` tightens the padding, gaps, icon tile and title for sidebars, popovers and narrow panels.

```tsx title="components/examples/empty/small.tsx"
import { IconInbox } from "@tabler/icons-react"

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

export function EmptySmall() {
  return (
    <div className="w-full max-w-xs">
      <Empty variant="outline" size="sm">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <IconInbox />
          </EmptyMedia>
          <EmptyTitle>No messages</EmptyTitle>
          <EmptyDescription>
            Messages from your team land here.
          </EmptyDescription>
        </EmptyHeader>
        <EmptyContent>
          <Button size="sm">New message</Button>
        </EmptyContent>
      </Empty>
    </div>
  )
}
```

### Avatar

The default `EmptyMedia` variant only centers its content, so any avatar, badge or illustration fits.

```tsx title="components/examples/empty/avatar.tsx"
import { Avatar, AvatarBadge, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyAvatar() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia>
          <Avatar size="lg">
            <AvatarFallback>LH</AvatarFallback>
            <AvatarBadge status="offline" />
          </Avatar>
        </EmptyMedia>
        <EmptyTitle>Lena is offline</EmptyTitle>
        <EmptyDescription>
          Leave a message and she’ll see it when she’s back.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button size="sm">Leave a message</Button>
      </EmptyContent>
    </Empty>
  )
}
```

### Avatar group

Show who could be here, for invites and shared spaces.

```tsx title="components/examples/empty/avatar-group.tsx"
import { IconPlus } from "@tabler/icons-react"

import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyAvatarGroup() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia>
          <AvatarGroup>
            <Avatar>
              <AvatarFallback>AL</AvatarFallback>
            </Avatar>
            <Avatar>
              <AvatarFallback>GH</AvatarFallback>
            </Avatar>
            <Avatar>
              <AvatarFallback>AT</AvatarFallback>
            </Avatar>
          </AvatarGroup>
        </EmptyMedia>
        <EmptyTitle>No team members</EmptyTitle>
        <EmptyDescription>
          Invite your team to collaborate on this project.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button size="sm">
          <IconPlus data-icon="inline-start" />
          Invite members
        </Button>
      </EmptyContent>
    </Empty>
  )
}
```

### Image

Give images explicit dimensions so the layout doesn’t shift while they load.

```tsx title="components/examples/empty/image.tsx"
import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyImage() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia>
          <img
            src="/preview/landscape.svg"
            alt=""
            width={160}
            height={100}
            className="h-24 w-40 rounded-lg object-cover"
          />
        </EmptyMedia>
        <EmptyTitle>No photos in this album</EmptyTitle>
        <EmptyDescription>
          Drop photos here or add them from your library.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button variant="outline" size="sm">
          Add photos
        </Button>
      </EmptyContent>
    </Empty>
  )
}
```

### No search results

Swap the results for an empty state when filters match nothing, and offer a way out. A visually hidden `role="status"` announces the count, so screen reader users hear the change without the whole list being read.

```tsx title="components/examples/empty/search-results.tsx"
"use client"

import * as React from "react"
import { IconFilterOff } from "@tabler/icons-react"

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

const tasks = [
  { title: "Write release notes", done: true },
  { title: "Review pull requests", done: true },
  { title: "Plan the next sprint", done: false },
]

export function EmptySearchResults() {
  const [onlyOpen, setOnlyOpen] = React.useState(true)
  const visible = tasks.filter((task) => !onlyOpen || !task.done)
  const [query, setQuery] = React.useState("sprint")
  const results = visible.filter((task) =>
    task.title.toLowerCase().includes(query.toLowerCase())
  )

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex flex-wrap gap-2">
        <Button
          variant={query ? "secondary" : "outline"}
          size="sm"
          onClick={() => setQuery(query ? "" : "sprint")}
        >
          {query ? `Search: ${query}` : "No search"}
        </Button>
        <Button
          variant={onlyOpen ? "secondary" : "outline"}
          size="sm"
          onClick={() => setOnlyOpen(!onlyOpen)}
        >
          {onlyOpen ? "Open tasks only" : "All tasks"}
        </Button>
        <Button variant="outline" size="sm" onClick={() => setQuery("design")}>
          Search “design”
        </Button>
      </div>
      <p role="status" className="sr-only">
        {results.length === 1 ? "1 task" : `${results.length} tasks`}
      </p>
      <div>
        {results.length > 0 ? (
          <ul className="flex flex-col gap-1 rounded-xl border p-2 text-sm">
            {results.map((task) => (
              <li key={task.title} className="rounded-md px-2 py-1.5">
                {task.title}
              </li>
            ))}
          </ul>
        ) : (
          <Empty variant="outline" size="sm">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <IconFilterOff />
              </EmptyMedia>
              <EmptyTitle>No matching tasks</EmptyTitle>
              <EmptyDescription>
                Nothing matches “{query}” with the current filters.
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Button
                variant="outline"
                size="sm"
                onClick={() => {
                  setQuery("")
                  setOnlyOpen(false)
                }}
              >
                Clear filters
              </Button>
            </EmptyContent>
          </Empty>
        )}
      </div>
    </div>
  )
}
```

### In a card

Use `size="sm"` with the muted variant to fill a card that has no data yet.

```tsx title="components/examples/empty/in-card.tsx"
import { IconChartBar } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyInCard() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Revenue</CardTitle>
        <CardDescription>Last 30 days</CardDescription>
      </CardHeader>
      <CardContent>
        <Empty variant="muted" size="sm">
          <EmptyHeader>
            <EmptyMedia variant="icon">
              <IconChartBar />
            </EmptyMedia>
            <EmptyTitle>No data yet</EmptyTitle>
            <EmptyDescription>
              Charts appear after your first sale.
            </EmptyDescription>
          </EmptyHeader>
          <EmptyContent>
            <Button variant="outline" size="sm">
              Connect a store
            </Button>
          </EmptyContent>
        </Empty>
      </CardContent>
    </Card>
  )
}
```

### In a table

Put it in a single cell that spans every column with `colSpan`, so the header stays aligned.

```tsx title="components/examples/empty/in-table.tsx"
import { IconReceipt } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

export function EmptyInTable() {
  return (
    <div className="w-full">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Invoice</TableHead>
            <TableHead>Customer</TableHead>
            <TableHead>Amount</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          <TableRow>
            <TableCell colSpan={3}>
              <Empty size="sm">
                <EmptyHeader>
                  <EmptyMedia variant="icon">
                    <IconReceipt />
                  </EmptyMedia>
                  <EmptyTitle>No invoices</EmptyTitle>
                  <EmptyDescription>
                    Invoices you send will show up here.
                  </EmptyDescription>
                </EmptyHeader>
                <EmptyContent>
                  <Button size="sm">Create invoice</Button>
                </EmptyContent>
              </Empty>
            </TableCell>
          </TableRow>
        </TableBody>
      </Table>
    </div>
  )
}
```

### In a popover

A compact empty state for menus and notification panels.

```tsx title="components/examples/empty/in-popover.tsx"
import { IconBell, IconBellOff } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export function EmptyInPopover() {
  return (
    <Popover>
      <PopoverTrigger
        render={
          <Button variant="outline" size="icon" aria-label="Notifications" />
        }
      >
        <IconBell />
      </PopoverTrigger>
      <PopoverContent className="w-72">
        <Empty size="sm">
          <EmptyHeader>
            <EmptyMedia variant="icon">
              <IconBellOff />
            </EmptyMedia>
            <EmptyTitle>Nothing new</EmptyTitle>
            <EmptyDescription>
              We’ll let you know when something needs your attention.
            </EmptyDescription>
          </EmptyHeader>
        </Empty>
      </PopoverContent>
    </Popover>
  )
}
```

### In a command list

`<CommandEmpty />` passes the current search to a function child, so the message can quote it.

```tsx title="components/examples/empty/in-command.tsx"
"use client"

import * as React from "react"
import { IconSearch } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

export function EmptyInCommand() {
  const [search, setSearch] = React.useState("zzz")

  return (
    <Command className="w-full max-w-sm">
      <CommandInput
        placeholder="Search people…"
        value={search}
        onValueChange={setSearch}
      />
      <CommandList>
        <CommandEmpty>
          {(search) => (
            <Empty size="sm">
              <EmptyHeader>
                <EmptyMedia variant="icon">
                  <IconSearch />
                </EmptyMedia>
                <EmptyTitle>No results</EmptyTitle>
                <EmptyDescription>
                  Nothing matches “{search}”. Try another name.
                </EmptyDescription>
              </EmptyHeader>
            </Empty>
          )}
        </CommandEmpty>
        <CommandGroup heading="People">
          <CommandItem>Ada Lovelace</CommandItem>
          <CommandItem>Alan Turing</CommandItem>
          <CommandItem>Grace Hopper</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
```

### Heading level

The title is a `<div>` so it never breaks your page outline. Render it as the heading level that fits, and label the region with it.

```tsx title="components/examples/empty/heading.tsx"
import { IconFileText } from "@tabler/icons-react"

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

export function EmptyHeading() {
  return (
    <Empty render={<section aria-labelledby="drafts-empty-title" />}>
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <IconFileText />
        </EmptyMedia>
        <EmptyTitle id="drafts-empty-title" render={<h3 />}>
          No drafts
        </EmptyTitle>
        <EmptyDescription render={<p />}>
          Drafts are saved automatically while you write.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button size="sm">Write a post</Button>
      </EmptyContent>
    </Empty>
  )
}
```

### Long content

Unbroken names and long addresses wrap inside the container, and long button labels truncate.

```tsx title="components/examples/empty/long-content.tsx"
import { IconAlertTriangle } from "@tabler/icons-react"

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

export function EmptyLongContent() {
  return (
    <div className="w-full max-w-72">
      <Empty variant="outline" size="sm">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <IconAlertTriangle />
          </EmptyMedia>
          <EmptyTitle>
            Nothing found in
            averyveryverylongunbrokenworkspacenamethatkeepsgoing
          </EmptyTitle>
          <EmptyDescription>
            <p>
              We searched every folder shared with
              someone.with.a.really.long.address@example-company.com and found
              nothing.
            </p>
            <p>
              Check the <a href="#">sharing settings</a> or ask the owner for
              access.
            </p>
          </EmptyDescription>
        </EmptyHeader>
        <EmptyContent>
          <Button variant="outline" size="sm" className="max-w-full">
            <span className="truncate">
              Request access from the workspace owner
            </span>
          </Button>
        </EmptyContent>
      </Empty>
    </div>
  )
}
```

### Right to left

Everything is centered and uses logical properties, so it reads naturally in right-to-left layouts.

```tsx title="components/examples/empty/rtl.tsx"
import { IconFolderPlus } from "@tabler/icons-react"

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

export function EmptyRtl() {
  return (
    <div dir="rtl" className="w-full">
      <Empty variant="outline">
        <EmptyHeader>
          <EmptyMedia variant="icon">
            <IconFolderPlus />
          </EmptyMedia>
          <EmptyTitle>لا توجد مشاريع بعد</EmptyTitle>
          <EmptyDescription>
            لم تنشئ أي مشروع حتى الآن. ابدأ مشروعًا جديدًا أو استورد مستودعًا
            موجودًا. <a href="#">اعرف المزيد</a>
          </EmptyDescription>
        </EmptyHeader>
        <EmptyContent>
          <div className="flex flex-wrap justify-center gap-2">
            <Button>إنشاء مشروع</Button>
            <Button variant="outline">استيراد</Button>
          </div>
        </EmptyContent>
      </Empty>
    </div>
  )
}
```

## Accessibility

- `<EmptyTitle />` renders a `<div>`. Pass `render={<h2 />}` (or the level that fits) when the empty state is a section of the page.
- `<EmptyMedia variant="icon" />` is hidden from assistive tech because the title already says what it means. Images in the default variant stay exposed, so give them an `alt` or `alt=""`.
- When results disappear after a search or filter, announce the change with a short `role="status"` message next to the list. Don’t make the whole empty state a live region, since its buttons and links would be read every time.

## API reference

Every part renders a plain element, accepts its attributes and supports the `render` prop to swap it.

### Empty

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline" \| "muted"` | `"default"` |  |
| `size` | `"default" \| "sm"` | `"default"` | sm tightens padding, gaps, the icon tile and the title. |
| `animated` | `boolean` | `true` | Fade the parts up in sequence when the empty state mounts. Skipped under reduced motion. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="empty"` | Target the root in CSS. |
| `data-variant` | The current variant. |
| `data-size` | The current size. |
| `data-animated` | Present when the entrance animation is on. |
| `--empty-padding` | Padding on every side. Set by size. |
| `--empty-gap` | Space between the header and content. Set by size. |

### EmptyHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="empty-header"` | Target the header in CSS. |

### EmptyMedia

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "icon" \| "stack"` | `"default"` | icon puts an icon in a muted tile. stack raises the tile over two fanned cards that spread when the empty state is hovered. Both hide the media from assistive tech. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="empty-icon"` | Target the media in CSS. Matches shadcn. |
| `data-variant` | The current variant. |

### EmptyTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` | Render a heading, such as render={<h2 />}. |

| Attribute | Description |
| --- | --- |
| `data-slot="empty-title"` | Target the title in CSS. |

### EmptyDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` | Plain links inside are underlined. Components rendered as links keep their own style. |

| Attribute | Description |
| --- | --- |
| `data-slot="empty-description"` | Target the description in CSS. |

### EmptyContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="empty-content"` | Target the actions area in CSS. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
