Empty
A zero-data placeholder — media, title, description and actions — for an empty list, a cleared inbox, or a missing page.
- Status
- Since
0.1.0- Accessibility pattern
- heading + supporting content
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Empty before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify emptyInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/emptyRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
Usage
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Icon />
</EmptyMedia>
<EmptyTitle>No data</EmptyTitle>
<EmptyDescription>No data found</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Add data</Button>
</EmptyContent>
</Empty>;An empty state always shows an icon. Empty takes an icon prop and renders it in the icon
tile; with no icon and no composed EmptyMedia it falls back to lucide Inbox. A composed
EmptyMedia replaces the default. The tile is a 48px bordered muted plate with a 24px icon, and the
tile sizes whatever icon you pass, so never put a size-* class on it.
<Empty icon={<SearchX />}>
<EmptyHeader>
<EmptyTitle>No matches</EmptyTitle>
<EmptyDescription>
Try a different search or clear the filters.
</EmptyDescription>
</EmptyHeader>
</Empty>Compact. size="sm" is the small inline empty — "No tasks yet" inside a card — with a 16px
padding, an 8px gap and a 40px tile with a 20px icon. DataList's built-in noResults and
emptyState use the default size:
<Empty size="sm" icon={<ListTodo />}>
<EmptyHeader>
<EmptyTitle>No tasks yet</EmptyTitle>
</EmptyHeader>
</Empty>When to use which — in a DataList, never hand-roll either:
- Nothing yet (
DataList emptyState) — the list has no records at all. Say what is missing and offer the one create action. - No matches (
DataList noResults, orNoResultsEmptyelsewhere) — a search or filter matched nothing. SearchX, "No matches", a short description and "Clear filters" wired to theFilterRow'sonClear.
Which empty state a surface needs — nothing yet, no matches, couldn't load, blocked — and its default
copy is set by the empty-state tiers. A search or filter that
matched nothing is "No matches" with a "Clear filters" action, and a failed load is role="alert"
with "Try again".
Anatomy
Examples
Composition
Use the following composition to build an Empty state:
Empty
├── EmptyHeader
│ ├── EmptyMedia
│ ├── EmptyTitle
│ └── EmptyDescription
└── EmptyContentOutline
Use the border utility class to create an outline empty state — the root already carries
border-dashed, so border alone turns the dashed outline on.
Background
Use the bg-* and bg-gradient-* utilities to add a background to the empty state.
Avatar
Use the EmptyMedia component to display an avatar in the empty state.
Avatar Group
Use the EmptyMedia component to display an avatar group in the empty state.
InputGroup
EmptyContent takes an InputGroup, which keeps the leading icon
and the trailing Kbd hint in their own addons, outside the editable box.
RTL
Every part is written in logical properties, so a right-to-left document mirrors the media chip, the action row and the description without any extra work.
As a heading
EmptyTitle renders a div by default. Pass render to make it the real heading at the level
the page needs; the classes and the data-slot stay the same.
<EmptyTitle render={<h3 />}>No projects yet</EmptyTitle>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ReactNode | — | |
size | "default" | "sm" | — |
EmptyHeader adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "icon" | null | — |
EmptyTitle adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
EmptyDescription adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
EmptyContent adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Accessibility
- Every part renders a plain
<div>, so the empty state adds no landmark and no implicit heading. Where the placeholder replaces a named region, give the surrounding container the heading. EmptyTitleis styled text by default, not a heading element. When the empty state stands in for a section, passrender={<h2 />}(or the level the outline needs) so the title is the real heading (API-22).EmptyMediais decorative: the icon inside it carries no accessible name, and the title plus the description are what a screen reader announces.EmptyContentholds real controls — aButton, anInput, a link — and each keeps its own focus outline frombase.css; the placeholder never suppresses one.
| Contract | States tested |
|---|---|
| Behaviour | default, empty, error |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test |
| Visual | default, error, empty |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/empty.patch. Every hunk:
- TYP-15 — the title's
tracking-tightis dropped: the title istext-sm, in the copy tier, which the theme holds at zero tracking. - API-22 —
EmptyTitlerenders through Base UI'suseRender, so it takesrenderand the heading level is set on the part. The defaultdiv, thedata-slotand the classes are upstream's. - API-16 —
"use client"is added:useRendercallsReact.useRefinternally, so the module needs the client boundary to import in a Server Component. - API-29 —
Emptyalways renders an icon: aniconprop (lucideInboxby default) drawn as a leadingEmptyMedia variant="icon", hidden when the composition brings its ownEmptyMedia; andsize="sm"is the compact inline empty. - DOC-2 —
cnis imported from@vegastack/design. - LAY-17 —
EmptyTitleandEmptyDescriptiontakewrap-anywhere: a long unbroken word (a quoted search, a long name) breaks inside the empty state instead of running past it.