Tessina UI
Tessina UI
GitHubIntroduction
InstallationUsageTheming
Components
ButtonIconButtonLinkSpinnerBadgeAvatarStatusChipShortcutSkeletonSurfaceProgressMeterRating
LabelFieldFieldsetCheckboxRadioSwitchSliderSelectComboboxSearchTextareaNumberFieldDate PickerTime PickerOTP InputFile UploadCalendar
AlertBannerToastTooltip
TabsAccordionCollapsibleBreadcrumbPaginationStepperSegmentedControlButtonGroupToggleButtonToggleGroupToolbarNavigation MenuMenubarBottom NavSidebar
Split ButtonFABDropdown MenuContextMenuCommandPopoverHoverCard
ContainerStackFlexGridAspectRatioSpacerCardCarouselDividerScroll Area
Table
ChatBubblePromptInputCodeBlock
ModalAlertDialogDrawerAction SheetTop Header MobileTop Header DesktopEmptyStateForm
Contributing
ComponentsSurfaces

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

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 rolestatusregionalert

API Reference

Props

PropTypeDefaultDescription
titleReactNode—Headline
descriptionReactNode—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
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"status"ARIA role — defaults to status so screen readers announce the empty state
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 intent="primary" 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.
  • Accessibility — defaults to role="status" so screen readers announce the empty state when it appears. Override with role="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.

On this page

PlaygroundInstallationUsageShowcaseEmptyState vs Banner vs AlertAPI ReferencePropsEmptyStateActionNotes