# Empty

Displays an empty state.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"
import { HStack } from "@/components/ui/stack"
import { FolderCode } from "lucide-react"

const styles = stylex.create({
  empty: { width: "100%" },
})

export default function EmptyDemo() {
  return (
    <Empty xstyle={styles.empty}>
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <FolderCode />
        </EmptyMedia>
        <EmptyTitle>No projects yet</EmptyTitle>
        <EmptyDescription>
          You haven&apos;t created any projects yet. Get started by creating your first project.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <HStack gap={2}>
          <Button>Create project</Button>
          <Button variant="secondary">Import project</Button>
        </HStack>
      </EmptyContent>
    </Empty>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and
   `breakpoints.stylex.ts` from the same page. The icon tile comes from
   `states.ts`. `Empty` also needs `interop.css`, which sizes the icon.

3. Copy and paste the following code into your project.

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

   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { breakpoints } from "@/theme/breakpoints.stylex"
   import { states } from "@/theme/states"
   import { color, leading, radius, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     root: {
       display: "flex",
       flexDirection: "column",
       alignItems: "center",
       justifyContent: "center",
       flexGrow: 1,
       minWidth: 0,
       rowGap: space.s6,
       padding: { default: space.s6, [breakpoints.md]: space.s12 },
       borderRadius: radius.xl,
       backgroundColor: color.tray,
       color: color.foreground,
       textAlign: "center",
       textWrap: "balance",
     },
     header: {
       display: "flex",
       flexDirection: "column",
       alignItems: "center",
       rowGap: space.s1,
       maxWidth: "24rem",
     },
     media: {
       display: "flex",
       alignItems: "center",
       justifyContent: "center",
       flexShrink: 0,
       marginBlockEnd: space.s3,
     },
     title: {
       fontSize: text.sm,
       lineHeight: leading.sm,
       fontWeight: 500,
     },
     description: {
       color: color.mutedForeground,
       fontSize: text.sm,
       lineHeight: leading.sm,
     },
     content: {
       display: "flex",
       flexDirection: "column",
       alignItems: "center",
       rowGap: space.s4,
       width: "100%",
       maxWidth: "24rem",
       minWidth: 0,
       fontSize: text.sm,
       lineHeight: leading.sm,
     },
   })

   const mediaVariants = stylex.create({
     default: {},
     icon: {
       width: space.s10,
       height: space.s10,
       "--carbon-icon-size": space.s5,
     },
   })

   const emptyStyles = {
     base: styles.root,
     header: styles.header,
     media: styles.media,
     mediaVariants: { default: mediaVariants.default, icon: [states.iconTile, mediaVariants.icon] },
     title: styles.title,
     description: styles.description,
     content: styles.content,
   }

   type EmptyMediaVariant = keyof typeof mediaVariants

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type DivProps = useRender.ComponentProps<"div"> & Overrides

   function Empty({ render, ref, style, xstyle, className, ...props }: DivProps) {
     const sx = stylex.props(styles.root, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "empty",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function EmptyHeader({ render, ref, style, xstyle, className, ...props }: DivProps) {
     const sx = stylex.props(styles.header, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "empty-header",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function EmptyMedia({
     variant = "default",
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: DivProps & { variant?: EmptyMediaVariant }) {
     const sx = stylex.props(
       styles.media,
       variant === "icon" && states.iconTile,
       mediaVariants[variant],
       xstyle,
     )
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "empty-media",
         "data-variant": variant,
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function EmptyTitle({ render, ref, style, xstyle, className, ...props }: DivProps) {
     const sx = stylex.props(styles.title, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "empty-title",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function EmptyDescription({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"p"> & Overrides) {
     const sx = stylex.props(styles.description, xstyle)
     return useRender({
       defaultTagName: "p",
       render,
       ref,
       props: {
         "data-slot": "empty-description",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function EmptyContent({ render, ref, style, xstyle, className, ...props }: DivProps) {
     const sx = stylex.props(styles.content, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "empty-content",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   export {
     Empty,
     EmptyContent,
     EmptyDescription,
     EmptyHeader,
     EmptyMedia,
     EmptyTitle,
     emptyStyles,
     type EmptyMediaVariant,
   }
   ```

4. 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="icon">
      <Icon />
    </EmptyMedia>
    <EmptyTitle>No data</EmptyTitle>
    <EmptyDescription>No data found</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Add data</Button>
  </EmptyContent>
</Empty>
```

## Composition

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

`EmptyHeader` holds the media, title and description close together.
`EmptyContent` follows a wider gap below it.

## Avatar

Leave `EmptyMedia` on its default variant to show an avatar or an image of
your own instead of the icon tile.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty"

const styles = stylex.create({
  empty: { width: "100%" },
})

export default function EmptyAvatar() {
  return (
    <Empty xstyle={styles.empty}>
      <EmptyHeader>
        <EmptyMedia>
          <Avatar size="lg">
            <AvatarImage src="https://github.com/shadcn.png" alt="" />
            <AvatarFallback>CN</AvatarFallback>
          </Avatar>
        </EmptyMedia>
        <EmptyTitle>User offline</EmptyTitle>
        <EmptyDescription>
          This user is currently offline. You can leave a message to notify them or try again later.
        </EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button size="sm" variant="secondary">
          Leave message
        </Button>
      </EmptyContent>
    </Empty>
  )
}
```

## In a table

Inside a surface that already has a fill, such as a table's body, remove the
tray with `xstyle`.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { color, space } from "@/theme/tokens.stylex"
import { Plus, Webhook } from "lucide-react"

const styles = stylex.create({
  empty: { backgroundColor: color.transparent, paddingBlock: space.s10, whiteSpace: "normal" },
})

export default function EmptyTable() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Endpoint</TableHead>
          <TableHead>Events</TableHead>
          <TableHead align="end">Success rate</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        <TableRow>
          <TableCell colSpan={3}>
            <Empty xstyle={styles.empty}>
              <EmptyHeader>
                <EmptyMedia variant="icon">
                  <Webhook />
                </EmptyMedia>
                <EmptyTitle>No endpoints</EmptyTitle>
                <EmptyDescription>Add an endpoint to start receiving events.</EmptyDescription>
              </EmptyHeader>
              <Button size="sm" variant="secondary" iconPosition="start">
                <Plus />
                Add endpoint
              </Button>
            </Empty>
          </TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part renders through Base UI's
[`useRender`](https://base-ui.com/react/utils/use-render), and takes `xstyle`,
`className` and inline `style` alongside every attribute of its element.
`EmptyDescription` renders a `p`; every other part renders a `div`.

### Empty

A centred column on a tray. It grows to fill a flex parent, and its padding
widens from `1.5rem` to `3rem` at the `md` breakpoint.

### EmptyHeader

A column with a `0.25rem` gap, at most `24rem` wide.

### EmptyMedia

| Prop      | Type                  | Default     |
| --------- | --------------------- | ----------- |
| `variant` | `"default" \| "icon"` | `"default"` |

`icon` puts the glyph on a `2.5rem` tile and sizes it to `1.25rem`. `default`
centres whatever it holds.

### EmptyTitle

Weight 500 at the body size.

### EmptyDescription

Muted text at the body size.

### EmptyContent

A column with a `1rem` gap for actions or anything else, at most `24rem` wide.

### emptyStyles

The style maps behind the component. Spread them onto any element.

| Key             | What it is                                       |
| --------------- | ------------------------------------------------ |
| `base`          | The tray and its centred column                  |
| `header`        | The media, title and description column          |
| `media`         | The media's box                                  |
| `mediaVariants` | `default`, and `icon` with the shared `iconTile` |
| `title`         | The title's type                                 |
| `description`   | The muted description                            |
| `content`       | The column below the header                      |

### Data attributes

| Attribute                       | On              | Effect                                |
| ------------------------------- | --------------- | ------------------------------------- |
| `data-slot="empty"`             | the root        | Targets the root from a parent        |
| `data-slot="empty-header"`      | the header      | Targets the header from a parent      |
| `data-slot="empty-media"`       | the media       | Targets the media from a parent       |
| `data-variant`                  | the media       | Reflects the media's `variant` prop   |
| `data-slot="empty-title"`       | the title       | Targets the title from a parent       |
| `data-slot="empty-description"` | the description | Targets the description from a parent |
| `data-slot="empty-content"`     | the content     | Targets the content from a parent     |

### CSS variables

| Variable             | On        | Effect                                                 |
| -------------------- | --------- | ------------------------------------------------------ |
| `--carbon-icon-size` | the media | The icon's size, read by `interop.css`. 20px on `icon` |
