Empty
An empty-state placeholder composed from header, media, title, description and content parts, with a default or icon media variant.
import { FolderOpen } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
export default function EmptyDemo() {
return (
<Empty className="w-full max-w-sm rounded-xl border border-border">
<EmptyHeader>
<EmptyMedia variant="icon">
<FolderOpen />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>
Create your first project to start shipping components.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button tone="brand">New project</Button>
</EmptyContent>
</Empty>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/emptyDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/empty.tsximport type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
)
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className
)}
{...props}
/>
)
}
const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6 [&_i]:text-2xl [&_i]:leading-none",
},
},
defaultVariants: {
variant: "default",
},
}
)
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
)
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<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}
/>
)
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
)
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
emptyMediaVariants,
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the muted surface colors will be missing.
Usage
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
import { FolderOpen } from "lucide-react"
<Empty className="border border-dashed">
<EmptyHeader>
<EmptyMedia variant="icon">
<FolderOpen />
</EmptyMedia>
<EmptyTitle>No projects yet</EmptyTitle>
<EmptyDescription>
Create your first project to start shipping components.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button tone="brand">New project</Button>
</EmptyContent>
</Empty>Every part is optional. Compose only what you need, and add a border utility through className when you want the framed placeholder look.
Examples
Media variants
default is a transparent wrapper for a sized glyph; icon wraps a single icon in a fixed muted tile.
With actions
Inline, no border
Props
The only axis lives on EmptyMedia. Empty, EmptyHeader, EmptyTitle, EmptyDescription and EmptyContent are plain <div> wrappers and accept every native div prop.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "icon" | "default" | Media treatment on EmptyMedia. default is a transparent wrapper for a large glyph or illustration; icon renders a fixed muted tile sized for a single icon. |
ai2 Empty: an empty-state placeholder for React
The ai2 Empty is a shadcn-compatible empty-state component for React, styled with Tailwind CSS v4 on top of the shared ai2 token file. It is the surface you show when a list, table or search returns nothing: centered, balanced text with an optional media tile, a title, a description and a slot for actions.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Empty?
It is a six-part composition: Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription and EmptyContent. Every part is a plain div, so the component is server-safe and free of client-side JavaScript.
The root centers its children, balances the text and reserves generous padding that grows on medium screens. The only cva axis is the media variant on EmptyMedia: default for a bare glyph and icon for a muted rounded tile. The dashed border is opt-in, added through className, so the same component fits both a framed placeholder and a borderless section.
Why use it
- Composable anatomy: Header, media, title, description and content are all optional. A minimal empty state is a title plus a description; a rich one adds a media tile and action buttons.
- Two media variants: EmptyMedia variant="default" is a transparent wrapper for a large illustration or glyph, and variant="icon" is a fixed muted tile that keeps a single icon perfectly centered.
- Balanced, centered text: The root applies text-balance and centering, so titles and descriptions wrap evenly without manual line breaks.
- Server component friendly: The source has no "use client" directive and no state, so it renders in React Server Components and ships zero component JavaScript.
- Agent-readable metadata: The registry item describes the parts and the media variant in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component and the @ai2/tokens theme to your project.
- Dashed-border ready: The root already carries border-dashed styling, so adding a border utility through className turns it into the classic framed placeholder without extra CSS.
- Icon and remixicon compatible: The icon media tile sizes a lucide <svg> and a remixicon <i> child the same way, so it works with either icon family.
- Data attributes for styling: Every part exposes its own data-slot, and EmptyMedia adds data-variant, so CSS overrides can target exact regions.
- Exported emptyMediaVariants: The cva function for the media tile is exported, so you can reuse the same treatment on another element.
- TypeScript source: Every part extends the native div props, and EmptyMedia adds the cva variant, so autocomplete covers the full API in your editor.
Production tips
- Add your own border: The root ships border-dashed but no border width. Add border or border border-dashed through className to draw the frame.
- Pick the media variant by content: Use variant="icon" for a single lucide icon and variant="default" for a larger illustration or a sized glyph that should not sit inside a tile.
- Keep actions in EmptyContent: EmptyContent is a centered column with a small gap, made for one or two buttons or a short form. It keeps every empty state aligned.
- Keep heading semantics yourself: EmptyTitle is a styled div, not an h-tag. If the title matters for the document outline, render an h2 or h3 inside it.
Works with the rest of ai2
Empty pairs naturally with the rest of the registry. Put ai2 Button actions in the content slot, and reach for it whenever an ai2 Item list, a ai2 Table or a search result comes back with nothing to show.