- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
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:
- class-variance-authority
- cn
$ pnpm add class-variance-authority cnUsage
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.
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.
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
| Prop | Type / Description | Default |
|---|---|---|
| children | ReactNodeAll parts: compose content explicitly. Header groups Media, Title, and Description; Content holds actions or inputs. | Not set |
| className | stringAll parts: merged with defaults. Add border to Empty for a dashed outline; no border width is enabled by default. | Not set |
| description | ReactNode (composition)EmptyDescription children, not a prop on Empty. | Not set |
| outline | boolean (demo only)Demo composition: adds className="border" to Empty; not a component prop. | false (demo) |
| showAction | boolean (demo only)Demo composition: includes EmptyContent with a documentation link. Replace its destination in your application. | true (demo) |
| showMedia | boolean (demo only)Demo composition: includes EmptyMedia and a decorative folder icon. | true (demo) |
| title | ReactNode (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.