For the complete documentation index, see 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.
1
Support

Empty

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

Playground

Live preview
No projects yet
Create your first project to get started.
Media, action, and outline controls compose children and classes, not extra Empty props. Reset restores all settings. Replace the demo link destination in your app.
Customize Empty
Customize
empty-demo.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

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

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/empty.json

Dependencies

Installed automatically by the registry CLI. For manual setup:

Dependencies2 required packages
  • class-variance-authority
  • cn
$ pnpm add class-variance-authority cn

Usage

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

First-use guidance

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

No projects yet
Create your first project to get started.
empty-demo.tsx
import { FolderIcon } from "lucide-react";

import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} 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>
  </Empty>
);

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.

No matching components
Try a different keyword or browse the full component list.
empty-outline-demo.tsx
import { SearchIcon } from "lucide-react";
import Link from "next/link";

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

export const EmptyOutlineDemo = () => (
  <Empty className="border">
    <EmptyHeader>
      <EmptyMedia variant="icon">
        <SearchIcon aria-hidden="true" />
      </EmptyMedia>
      <EmptyTitle>No matching components</EmptyTitle>
      <EmptyDescription>
        Try a different keyword or browse the full component list.
      </EmptyDescription>
    </EmptyHeader>
    <EmptyContent>
      <Link
        href="/docs/components"
        className="rounded-sm underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-4"
      >
        Browse components
      </Link>
    </EmptyContent>
  </Empty>
);

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 separately if composing a Vandor Button; Empty does not install it automatically. Examples using icons also need lucide-react.

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:

$ pnpm dlx 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

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

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
childrenReactNode

All parts: compose content explicitly. Header groups Media, Title, and Description; Content holds actions or inputs.

Not set
classNamestring

All parts: merged with defaults. Add border to Empty for a dashed outline; no border width is enabled by default.

Not set
descriptionReactNode (composition)

EmptyDescription children, not a prop on Empty.

Not set
outlineboolean (demo only)

Demo composition: adds className="border" to Empty; not a component prop.

false (demo)
showActionboolean (demo only)

Demo composition: includes EmptyContent with a documentation link. Replace its destination in your application.

true (demo)
showMediaboolean (demo only)

Demo composition: includes EmptyMedia and a decorative folder icon.

true (demo)
titleReactNode (composition)

EmptyTitle children, not a prop on Empty. The title renders a div, not a heading.

Not set
variant"default" | "icon"

EmptyMedia: transparent media container or muted icon tile. Only affects the preview when media is shown.

"default"

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

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

  • shadcn/ui: Adapted Base Nova Empty composition and styling, with the website-specific font class removed and Description props aligned with its rendered div.