Skip to content
ilinxa/pro-ui

Empty State

alphav0.1.0

The designed answer for empty surfaces — icon or illustration, title, description, capability-gated actions, and a hint, across 6 variants and 3 sizes.

Category: FeedbackUpdated: 2026-08-12Created: 2026-08-12Author: ilinxa

Context

General-purpose empty-state for any surface that can render nothing: empty tables/lists, no-results search, failed fetches, offline banners, permission walls, first-run onboarding. The host decides when it's empty and supplies copy + handlers via props; this component only renders the answer — no data fetching, no state detection. Distinct from `DetailPanelEmptyState` (detail-panel's internal empty view) — no cross-dependency either direction, composition happens at the host (decision #35). Single-unit presentational widget: stateless, no context, capability-gated action slots (omit a handler and its button/link is absent from the DOM).

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/empty-state

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/empty-state-fixtures

Preview

No projects yet

Projects you create will show up here.

No results for “gantt”

Try a different term or clear the active filters.

Couldn't load this page

Something went wrong on our end. Try again in a moment.

You're offline

Changes will sync automatically once you're back online.

You don't have access

Ask a workspace admin to grant you access to this board.

Upload your first file

Drag and drop anywhere, or browse from your device.

Click any Primary button to log the action.

Demo source

demo.tsxtsx

Usage

When to use

Reach for EmptyState anywhere a surface can render nothing: an empty table/list, a no-results search, a failed fetch, an offline banner, a permission wall, or first-run onboarding. The host decides whenit's empty and supplies copy + handlers;EmptyState only renders the designed answer — icon or illustration, title, description, up to two actions, and an optional footer hint. Omit action/secondaryAction for a read-only variant — the buttons simply don't render.

Host example — search-empty inside a data table

import { EmptyState } from "@/components/empty-state";

export function ProjectsTable({ rows, filters, onClearFilters }) {
  if (rows.length === 0 && filters.active) {
    return (
      <EmptyState
        variant="search"
        size="sm"
        title={`No results for "${filters.query}"`}
        description="Try a different term or clear the active filters."
        action={{ label: "Clear filters", onClick: onClearFilters }}
      />
    );
  }

  return <Table rows={rows} />;
}

Variants

Six semantic tones, each with its own default lucide icon — override with icon, or replace the tile entirely with media (an illustration slot; when set, the icon and its halo are omitted).

  • default — no data yet (Inbox).
  • search — no results for a filter/query (SearchX); pairs with a "Clear filters" action.
  • error — something failed (TriangleAlert); pairs with a "Retry" action.
  • offline — no connection (WifiOff).
  • permission — locked content (Lock).
  • first-use — onboarding tone (Sparkles), decorative spinning dashed ring around the tile.

Actions

action / secondaryAction accept either an object — { label, onClick?, href?, disabled? } — rendered as a real Button (or a plain <a> styled via buttonVariants when href is set), or any ReactNode for full control over the rendered element.

Sizes

sm for inline/card-level empties, md for a section, lg for a full-page state. Controls type scale, icon-tile scale, and vertical padding.

Frame

frame="dashed" adds a dropzone-style dashed border; frame="card" raises the root onto a bg-card surface. Default "none"renders bare (for slotting into a table body or an already-framed container). A real drag-and-drop dropzone is a separate future component — don't wire drop handlers onto frame="dashed" alone.

Accessibility

  • Title renders a real heading (headingLevel, default 3) so empty states participate in the page outline.
  • role="status" aria-live="polite" is applied only for the transient variants — search, error, offline — where content changed after a user action. Static walls (permission, first-use) stay plain landmarks; announcing them on mount would be noise.
  • The icon tile, bloom, and decorative ring are aria-hidden. Actions are real, keyboard-reachable buttons/links.
  • Entrance animation uses motion-safe: variants — no motion for prefers-reduced-motion users, or when animated={false}.

Features

  • 6 semantic variants — default / search / error / offline / permission / first-use — each with its own default lucide icon and tone
  • 3 sizes (sm / md / lg) — type scale, icon-tile scale, and padding
  • icon slot (overrides the variant default) or media slot (illustration; replaces the icon tile + bloom entirely)
  • Capability-gated action + secondaryAction — object form renders a real Button (or a plain <a> via buttonVariants when href is set, never asChild), ReactNode form for full control; omit both and no button/link renders
  • Optional footer hint slot for micro-copy (e.g. a keyboard shortcut)
  • frame prop — 'none' (bare) / 'dashed' (dropzone-style border) / 'card' (raised bg-card surface)
  • Semantic heading level via headingLevel (1-6, default 3) so empties participate in the page outline
  • role='status' aria-live='polite' only on transient variants (search / error / offline); static walls (permission / first-use) stay silent landmarks
  • 60ms-staggered entrance reveal via tw-animate-css motion-safe utilities — no app-level reveal-up dependency, so consumers get the animation without the docs-site keyframe
  • animated={false} renders SSR-stable with no reveal classes; reduced-motion respected automatically
  • Decorative spinning dashed ring on the first-use variant's icon tile, disabled under reduced motion and when animated={false}
  • Design-token only — no hex/rgb literals, both themes

Tags

empty-statefeedbackemptyno-resultserror-stateofflinepermissiononboardingplaceholder

Dependencies

shadcn primitives: button
npm peer deps: lucide-react@^1.11.0