DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

EmptyState

Centered surface for "no data here" — empty lists, search results, tables, dashboards, inboxes, 404s, first-run / onboarding moments. Four variants, four sizes, semantic intents tinting the icon tile, primary + secondary actions, optional footer, heading control, LTR/RTL.

Playground

Installation

pnpm add @tessinaui/ui

Usage

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 />}
  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" }}
/>

Examples

Default

A simple empty state with an icon, title, description, and a primary action.

Variants

Four surface styles — plain, card, dashed, and soft (a borderless muted band, the Webflow/NotebookLM strip shape). On soft, muted text steps to text-foreground/75 and the neutral icon tile to bg-background, so both stay AA on the tinted plate.

Sizes

Four sizes — sm, md, lg, and xl — scale padding, icon tile, and type.

Intents

intent tints the leading icon tile while title and description stay neutral.

With actions

A primary action, a quieter secondary action, and a footer for alternative paths.

Loading

EmptyStateSkeleton reserves the same vertical rhythm while content loads. It is silent by default (house skeleton policy) — pass label="Loading…" to opt into a single role="status" announcement.

Minimal

The quiet end of the range — text-only, bare icon + caption, and the compact start-aligned in-panel state with a link-weight action. All are field-dominant shapes; icon, title, description and actions are each optional.

Illustration

image replaces the icon tile with a custom illustration. The media slot caps itself at max-w-full, so a wide graphic cannot overflow a narrow column.

Start-aligned

align="start" anchors copy and actions to the leading edge. Pair with titleAs when the empty state is the sole content of a region and its title belongs in the page outline.

Error and offline

Error/not-found/offline presets ship as recipes, not exports — intent plus your copy covers what Carbon's ErrorEmptyState/NotFoundEmptyState components hard-code. Three action tiers: filled retry, ghost secondary, and a footer for the error ID + support link.

EmptyState vs Banner vs Alert

EmptyStateBannerAlert
Purpose"No data here" — fills a content areaPromotional / informational — opt-in marketingReactive feedback — system event
LayoutAlways centered (or start), fills containerInline / centered / landscape, rides above contentInline, tight
ContainerPlain by default; card or dashed chromeAlways has surface chromeTight bordered/filled card
ExamplesEmpty inbox, no search results, 404, first-run"Unlock Premium", "7-Eleven loyalty""Failed to save", "Update available"
Default rolenone (static page content)regionderived per intent

Two boundary notes from the research set: an error that needs immediate action is a Banner/Alert, not an EmptyState (Astryx's rule); and the "+ New project" dashed create-tile in a grid (Framer, Replit, Frame.io) is a ghost create-button, not this component — variant="dashed" is the region shape, not the grid tile.

API Reference

Props

PropTypeDefaultDescription
titleReactNode—Headline
titleAs"div" | "p" | "h2"–"h6""div"Element the title renders as. Set an h2–h6 when the empty state is the sole content of a region (Atlassian headingLevel shape; house Alert titleAs precedent).
descriptionReactNode—Subhead / body copy. Has size-aware max-w so long copy wraps cleanly.
variant"plain" | "card" | "dashed" | "soft""plain"Surface chrome — plain for centered content with no border, card for a bordered surface, dashed for drop zones, soft for a borderless muted band
size"sm" | "md" | "lg" | "xl""md"Scales padding, gap, icon tile size, and font sizes
intent"none" | "error" | "warning" | "success" | "info""none"Tints the leading icon tile. Title and description stay neutral. ("primary" is a deprecated alias for one minor — §1 has no primary intent.)
rounded"none" | "sm" | "md" | "lg" | "full""lg"Container border radius. full caps at the §2 panel radius; the icon tile becomes a circle.
align"center" | "start""center"Content alignment
iconReactNode—Small leading icon — wrapped in a tinted square tile
imageReactNode—Larger illustration / image. Replaces icon when both passed.
actionEmptyStateAction | ReactNode—Primary CTA
secondaryActionEmptyStateAction | ReactNode—Quieter secondary CTA — defaults to ghost
footerReactNode—Small print, alternative paths, links
dir"ltr" | "rtl"inheritedText direction
roleAriaRole—No default. Pass role="status" only for a state that swaps in dynamically — and note a polite region must be mounted empty first to announce at all (see Accessibility).
classNamestring—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.

FieldTypeDescription
labelReactNodeButton text
onClick(event) => voidClick handler
hrefstringWhen 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
loadingbooleanShow spinner
disabledbooleanDisabled state
leadingIcon / trailingIconReactNodeIcons inside the button
classNamestringExtra classes merged onto the rendered button

Notes

  • Action defaults: when you pass an EmptyStateAction object, its variant, intent, size, and rounded default to sensible values for the empty state's intent / size. The common case (<EmptyState action={{ label: "Create" }} />) needs no extra tuning.
  • Icon vs image: icon becomes a tinted square tile coloured by intent. image is rendered as-is — pass any element. When both are passed, image wins.
  • Description sizing: description has size-aware max-w (max-w-xs to max-w-lg) and uses text-balance in 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. Use card when the empty state needs to stand alone (e.g., as a hero on a blank dashboard). Use dashed for drop zones / "click to upload" affordances.
  • RTL — works automatically via dir="rtl". Icon tile, text alignment, and action ordering all flip.
  • Copy — write the title as a short, positive statement of what goes here ("Start by adding data assets"), not what's missing, and don't word it like a button — a title isn't tappable (Carbon + Material guidance).
  • Accessibility — the root carries no role by default: no surveyed system mints a live region here, a role="status" that mounts with its content already in place announces nothing (polite regions only speak on content change), and a live region wrapping the action buttons would announce their internal churn. For a state that swaps in after a fetch, pass role="status" and make sure the region exists (empty) before the content lands. The icon tile is aria-hidden (decorative — the Astryx/Carbon consensus); the title defaults to a non-heading div like the house Alert — set titleAs for page-outline semantics. EmptyStateSkeleton is silent unless you pass label.
Dropdown MenuFAB

On this page

PlaygroundInstallationUsageExamplesDefaultVariantsSizesIntentsWith actionsLoadingMinimalIllustrationStart-alignedError and offlineEmptyState vs Banner vs AlertAPI ReferencePropsEmptyStateActionNotes