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…

import * as React from "react"
import { Inbox } from "lucide-react"
import { cn } from "@/lib/utils"

export interface EmptyStateAction {
  label: string
  /** Rendered as a real <button type="button"> — only works when EmptyState is mounted inside a Client Component. */
  onClick?: () => void
  /** Takes priority over onClick and renders a real <a> — fully Server Component safe. */
  href?: string
}

export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Defaults to a lucide Inbox glyph. Always rendered inside the same muted circle chip. */

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/empty-state.json

Prompt

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 href renders a plain <a>, so the whole panel can ship from a Server Component with zero hydration cost.
  • Client-boundary handoffonClick only 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; className decides whether the panel sits bare, inside a Card, or behind a dashed placeholder border.

On This Page