Display
Empty State
A centered empty-state panel — icon chip, title, description and up to two actions — for any non-data-contract blank surface.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/empty-state.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "EmptyState" component using lucide-react
for icons.
Contract
- Export a plain function component EmptyState with props:
- icon?: ReactNode (default: a lucide Inbox glyph)
- title: string
- description?: string
- action?: { label: string; onClick?: () => void; href?: string }
- secondaryAction?: same shape as action
- className, plus every native <div> prop spread onto the root.
Behavior
- The icon (default or custom) always renders inside the same size-12 rounded
circle chip — a consistent visual anchor regardless of which glyph is passed.
- title and description stack in a centered column; description is optional
and clamped to a readable max width.
- action / secondaryAction: when `href` is set, render a real <a> (link
semantics, no client runtime required); otherwise render a
<button type="button"> wired to `onClick`. action renders as the solid/
primary style, secondaryAction as the outline/secondary style. Render only
the ones passed — omit both to render a caption-only empty state with no
action row at all.
- No internal state, no timers, no animation — a pure render function.
Rendering & styling
- Semantic tokens only: bg-muted / text-muted-foreground for the icon chip,
bg-primary / text-primary-foreground for the primary action, border +
hover:bg-muted for the secondary action, text-muted-foreground for the
description. Merge the consumer's className onto the root via cn().
- No "use client" directive: the component has no hooks or browser APIs, so
it can render as a Server Component whenever every action uses `href`.
Customization levers
- Icon: swap in any node (lucide glyph, custom SVG, small illustration) — it's
always centered inside the same muted circle chip, so the overall composition
stays consistent regardless of glyph.
- Container: no dashed-border/card variant is baked in — apply your own
className (rounded-xl border, a dashed placeholder border, bg-card, drop it
inside a Card component, etc.) to match the surrounding surface.
- Density: override the root's py-12 px-6 and gap-4 via className for a
tighter or looser panel.
- Actions: supports 0, 1 or 2 actions. Pass only `secondaryAction` for a
single outline action, only `action` for a single solid CTA, or neither for
a caption-only state.Concepts
- RSC-safe link action — passing
hrefrenders a plain<a>, so the whole panel can ship from a Server Component with zero hydration cost. - Client-boundary handoff —
onClickonly fires because the consumer mounts EmptyState inside its own Client Component; EmptyState itself carries no"use client"and no hooks. - One consistent icon chip — whatever node is passed as
icon(default Inbox or a custom glyph) always sits inside the same muted circle, so visual weight stays consistent across every empty state in an app. - Optional action pair — zero, one, or two actions render conditionally; a bare title-and-description panel is a first-class state, not a fallback.
- Container left to the consumer — there's no built-in dashed-border/card variant;
classNamedecides whether the panel sits bare, inside aCard, or behind a dashed placeholder border.