# Empty

A composable shadcn-based empty state for missing data and first-use guidance.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

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

export const EmptyDemo = () => (
  <Empty>
    <EmptyHeader>
      <EmptyMedia variant="icon">
        <FolderIcon aria-hidden="true" />
      </EmptyMedia>
      <EmptyTitle>{"No projects yet"}</EmptyTitle>
      <EmptyDescription>{"Create your first project to get started."}</EmptyDescription>
    </EmptyHeader>
    <EmptyContent>
      <a href="/docs/components" className="rounded-sm underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-4">Explore components</a>
    </EmptyContent>
  </Empty>
);
```

## Installation [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/empty.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install class-variance-authority cn
```

## Usage [#usage]

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

<Empty>
  <EmptyHeader>
    <EmptyTitle>No projects yet</EmptyTitle>
    <EmptyDescription>
      Create your first project to get started.
    </EmptyDescription>
  </EmptyHeader>
</Empty>;
```

Use `EmptyHeader` to group optional `EmptyMedia`, `EmptyTitle`, and `EmptyDescription`. Put links, buttons, or inputs inside `EmptyContent`. Empty does not fetch data or decide when it is shown: render it after loading completes and there are no results.

## Examples [#examples]

### First-use guidance [#first-use-guidance]

Explain what is missing and how to get started. The icon is decorative; the text carries the meaning.

<ComponentPreview name="empty-demo" title="empty-demo.tsx">
  <EmptyDemo />
</ComponentPreview>

### Outline and next step [#outline-and-next-step]

Add `className="border"` to enable the existing dashed border style. The link below opens the component list. Replace it with a destination or a real application action appropriate to your empty state.

<ComponentPreview name="empty-outline-demo" title="empty-outline-demo.tsx">
  <EmptyOutlineDemo />
</ComponentPreview>

`EmptyMedia` defaults to a transparent container for arbitrary media. Use `variant="icon"` for a muted icon tile, or the default variant for an image or avatar. Media and actions are optional. Install [Button](/docs/components/button) separately if composing a Vandor Button; Empty does not install it automatically. Examples using icons also need `lucide-react`.

### Consumer setup and optional stories [#consumer-setup-and-optional-stories]

Requires React 19, Tailwind CSS 4, `class-variance-authority`, and `cn`. Load your global Tailwind stylesheet with shadcn semantic tokens (`muted`, `foreground`, `muted-foreground`, `primary`, `border`, and `ring` for example links). No Base UI runtime, provider, animation CSS, or client directive is required for Empty itself. The `new-york` source directory name does not change its Base Nova styling foundation.

The component and stories follow the UI alias configured in `components.json` (normally `components/ui`). Review the CLI overwrite prompt before replacing an existing Empty. For manual setup, install the declared dependencies and copy the Source into your UI directory; the examples and playground are not distributed with the component.

Install Empty first, then optionally install its portable stories:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/empty-stories.json
```

Use your existing `@storybook/react` setup with matching Storybook package versions (types and composition checked with 10.6.1). Discover `**/*.stories.tsx` in your UI directory and load your global Tailwind/theme stylesheet in the preview. No extra addons or providers are required for Canvas and Controls. Stories inherit the consumer theme and add no Storybook configuration or component dependencies. This project has no Storybook server; Storybook UI appearance is not verified here.

## Behavior & Accessibility [#behavior--accessibility]

Empty is a static layout, without state, callbacks, loading indicators, or animation. All six parts render `div` elements and forward native div attributes and React 19 refs. `EmptyTitle` is visual text, not an automatic semantic heading. For a named region, give Empty `role="region"` and `aria-labelledby` pointing to the title's `id`; add an appropriate heading role and `aria-level` to the title only when the page hierarchy calls for it.

Hide decorative icons with `aria-hidden="true"`, provide meaningful alternative text for informative images, and use native links or buttons for actions. Empty does not automatically announce async results or move focus. If results change dynamically, choose an application-level live region based on the interaction rather than making every empty state an alert. Keep loading, failed requests, and genuinely empty results distinct.

The playground's title and description are children of the corresponding parts. Outline, media visibility, and action visibility are demo composition controls, not extra component props. Generated code and Reset use the same settings as the preview.

## Props [#props]

| Prop        | Type                    | Default      | Description                                                                                                          |
| ----------- | ----------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------- |
| children    | ReactNode               | Not set      | All parts: compose content explicitly. Header groups Media, Title, and Description; Content holds actions or inputs. |
| className   | string                  | Not set      | All parts: merged with defaults. Add border to Empty for a dashed outline; no border width is enabled by default.    |
| description | ReactNode (composition) | Not set      | EmptyDescription children, not a prop on Empty.                                                                      |
| outline     | boolean (demo only)     | false (demo) | Demo composition: adds className="border" to Empty; not a component prop.                                            |
| showAction  | boolean (demo only)     | true (demo)  | Demo composition: includes EmptyContent with a documentation link. Replace its destination in your application.      |
| showMedia   | boolean (demo only)     | true (demo)  | Demo composition: includes EmptyMedia and a decorative folder icon.                                                  |
| title       | ReactNode (composition) | Not set      | EmptyTitle children, not a prop on Empty. The title renders a div, not a heading.                                    |
| variant     | "default" \| "icon"     | "default"    | EmptyMedia: transparent media container or muted icon tile. Only affects the preview when media is shown.            |

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

## Source [#source]

```tsx
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { cn } from "cn";
import type * as React from "react";

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

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

const emptyMediaVariants = cva(
  "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
  {
    defaultVariants: { variant: "default" },
    variants: {
      variant: {
        default: "bg-transparent",
        icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
      },
    },
  }
);

const EmptyMedia = ({
  className,
  variant = "default",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) => (
  <div
    data-slot="empty-icon"
    data-variant={variant}
    className={cn(emptyMediaVariants({ className, variant }))}
    {...props}
  />
);

const EmptyTitle = ({ className, ...props }: React.ComponentProps<"div">) => (
  <div
    data-slot="empty-title"
    className={cn("text-sm font-medium tracking-tight", className)}
    {...props}
  />
);

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

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

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

```

## Credits [#credits]

* [shadcn/ui](https://ui.shadcn.com/docs/components/base/empty): Adapted Base Nova Empty composition and styling, with the website-specific font class removed and Description props aligned with its rendered div.
