EmptyState
Centered surface for "no data here" — empty lists, search results, tables, dashboards, inboxes, 404s, first-run / onboarding moments. Three variants, four sizes, six intents tinting the icon tile, primary + secondary actions, optional footer, LTR/RTL.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { EmptyState } from "@tessinaui/ui";{/* Empty inbox */}
<EmptyState
icon={<Inbox />}
title="Inbox is empty"
description="When new messages arrive, they'll show up here."
action={{ label: "Compose", leadingIcon: <Plus /> }}
/>
{/* No search results — error intent */}
<EmptyState
icon={<SearchX />}
intent="error"
title="No matching results"
description="Try a different search term, or clear filters to see everything."
action={{ label: "Clear filters", variant: "outline", intent: "none" }}
secondaryAction={{ label: "Reset search" }}
/>
{/* Drop zone — dashed variant */}
<EmptyState
variant="dashed"
icon={<Upload />}
intent="primary"
title="Drop files here"
description="PNG, JPG, PDF up to 10 MB. Or click to browse."
action={{ label: "Choose files" }}
/>
{/* 404 / not found */}
<EmptyState
size="lg"
image={
<div className="size-20 rounded-full bg-error-light flex items-center justify-center">
<CloudOff className="size-10 text-error" />
</div>
}
title="Page not found"
description="The page you're looking for has been moved or no longer exists."
action={{ label: "Go home" }}
secondaryAction={{ label: "Contact support" }}
/>Showcase
EmptyState vs Banner vs Alert
| EmptyState | Banner | Alert | |
|---|---|---|---|
| Purpose | "No data here" — fills a content area | Promotional / informational — opt-in marketing | Reactive feedback — system event |
| Layout | Always centered (or start), fills container | Inline / centered / landscape, rides above content | Inline, tight |
| Container | Plain by default; card or dashed chrome | Always has surface chrome | Tight bordered/filled card |
| Examples | Empty inbox, no search results, 404, first-run | "Unlock Premium", "7-Eleven loyalty" | "Failed to save", "Update available" |
| Default role | status | region | alert |
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Headline |
description | ReactNode | — | Subhead / body copy. Has size-aware max-w so long copy wraps cleanly. |
variant | "plain" | "card" | "dashed" | "plain" | Surface chrome — plain for centered content with no border, card for a bordered surface, dashed for drop zones / "click to upload" |
size | "sm" | "md" | "lg" | "xl" | "md" | Scales padding, gap, icon tile size, and font sizes |
intent | "none" | "primary" | "error" | "warning" | "success" | "info" | "none" | Tints the leading icon tile. Title and description stay neutral. |
rounded | "none" | "sm" | "md" | "lg" | "xl" | "full" | "lg" | Container border radius (only meaningful for card and dashed variants) |
align | "center" | "start" | "center" | Content alignment |
icon | ReactNode | — | Small leading icon — wrapped in a tinted square tile |
image | ReactNode | — | Larger illustration / image. Replaces icon when both passed. |
action | EmptyStateAction | ReactNode | — | Primary CTA |
secondaryAction | EmptyStateAction | ReactNode | — | Quieter secondary CTA — defaults to ghost |
footer | ReactNode | — | Small print, alternative paths, links |
dir | "ltr" | "rtl" | inherited | Text direction |
role | AriaRole | "status" | ARIA role — defaults to status so screen readers announce the empty state |
className | string | — | Additional classes on the root |
EmptyStateAction
When you pass an object, the empty state renders a <Button> for you with sensible defaults. When you pass a ReactNode, that node is rendered as-is — useful for SplitButton, IconButton, custom links, etc.
| Field | Type | Description |
|---|---|---|
label | ReactNode | Button text |
onClick | (event) => void | Click handler |
href | string | When set, the button renders as <a href> via Button's render prop |
variant | "primary" | "secondary" | "ghost" | "outline" | Button variant |
intent | "none" | "error" | "warning" | "success" | "info" | Button intent |
size | "xs" | "sm" | "md" | "lg" | "xl" | Button size |
rounded | "none" | "sm" | "md" | "lg" | "full" | Button corner radius |
loading | boolean | Show spinner |
disabled | boolean | Disabled state |
leadingIcon / trailingIcon | ReactNode | Icons inside the button |
className | string | Extra classes merged onto the rendered button |
Notes
- Action defaults: when you pass an
EmptyStateActionobject, itsvariant,intent,size, androundeddefault to sensible values for the empty state'sintent/size. The common case (<EmptyState intent="primary" action={{ label: "Create" }} />) needs no extra tuning. - Icon vs image:
iconbecomes a tinted square tile coloured byintent.imageis rendered as-is — pass any element. When both are passed,imagewins. - Description sizing: description has size-aware
max-w(max-w-xstomax-w-lg) and usestext-balancein centered alignment so long copy wraps into nice symmetric lines instead of one long ragged line. - Variants: most empty states should be
plain— they fill an existing content area that already has its own surface. Usecardwhen the empty state needs to stand alone (e.g., as a hero on a blank dashboard). Usedashedfor drop zones / "click to upload" affordances. - RTL — works automatically via
dir="rtl". Icon tile, text alignment, and action ordering all flip. - Accessibility — defaults to
role="status"so screen readers announce the empty state when it appears. Override withrole="region"for static page-level empty states that shouldn't interrupt.
Top Header Desktop
Desktop navigation bar with brand, nav, search, and actions slots. Six variants, five sizes, six rounded options, three nav-position modes (start/center/end), max-width containment, bordered, sticky, skeleton, and full RTL support.
Form
A complete form layout — header, body, sections, rows, footer, submit error banner — built on Base UI's Form primitive with four variants, five sizes, intent accents, validation modes, and full RTL support.