# Empty State

> A React empty state for lists, tables, searches and pages with nothing to show yet: an icon tile, a title, a description and actions.

- Type: React component (Feedback)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add empty-state`
- Docs: https://wingo-ui.com/components/empty-state

## Interaction

It appears with the tile scaling up and the text and buttons rising in one after another; under reduced motion it only fades. Pick a preset to swap icon, tone and copy at once. Press the retry button on the error preset and it spins until the returned promise settles. Tab moves through the actions in reading order, Enter or Space presses them. With live on, a screen reader announces the title and description once when the state appears. On a phone, or anywhere narrower than 400px, two or more actions stack full width with the primary one on top, and lg pages drop to the md spacing below 640px.

## Installation

```bash
npx wingo-ui@latest add empty-state
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/empty-state.json`

Files:

- `components/ui/empty-state.tsx`

npm dependencies: `npm i @radix-ui/react-slot motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { FileUp, UserPlus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { EmptyState } from "@/components/ui/empty-state"

export function ClientsEmpty({ onReload }: { onReload: () => Promise<void> }) {
  return (
    <div className="flex flex-col gap-4">
      <EmptyState
        variant="surface"
        title="No clients yet"
        description="Add your first client or import the list from a CSV to get started."
        actions={
          <>
            <Button variant="outline" leftIcon={<FileUp />}>Import CSV</Button>
            <Button leftIcon={<UserPlus />}>Add client</Button>
          </>
        }
      />
      <EmptyState
        preset="error"
        size="sm"
        title="We could not load the invoices"
        description="Check your connection and try again."
        onRetry={onReload}
      />
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `preset?` | `"empty" \| "no-results" \| "error" \| "first-run" \| "no-permission" \| "offline"` | `"empty"` | Picks the default icon, tone and copy for the usual reasons: empty (Inbox, neutral), no-results (SearchX, neutral), error (CircleAlert, danger), first-run (Sparkles, brand, with a plus badge), no-permission (Lock, warning), offline (WifiOff, neutral). Every piece stays overridable. |
| `title?` | `ReactNode` | `""` | The heading. Leave empty for the preset's; null hides it. |
| `description?` | `ReactNode` | `""` | One or two sentences under the title; wraps at 36ch (44ch at lg). Leave empty for the preset's; null hides it. |
| `icon?` | `ReactNode` |  | The glyph inside the tile, sized by the empty state. Defaults to the preset's icon; null hides the tile. |
| `illustration?` | `ReactNode` |  | Replaces the tile with an image or an svg, at most 112 / 160 / 192px tall by size. Decorative (aria-hidden). |
| `badge?` | `ReactNode` |  | A small round badge on the tile's corner in the tone's fill. first-run shows a plus; null hides it. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `""` | The tile tint. Empty uses the preset's tone. neutral is a quiet gray tile; the others use the tone's soft fill and text. |
| `color?` | `string` | `""` | Any CSS color for the tile (and the badge); replaces the tone. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | sm inside a table, list or popover (40px tile, 14px title, 24px padding); md in a panel (48px tile, 15px title, 40px padding); lg for a whole page (64px tile, 18px title, 64px padding, 40px below 640px). |
| `variant?` | `"plain" \| "dashed" \| "surface"` | `"plain"` | plain sits on whatever is behind it; dashed draws a dashed frame (first-run areas, drop zones); surface is a raised panel. |
| `align?` | `"center" \| "start"` | `"center"` | start aligns text and actions to the start edge, for cards and sidebars. |
| `actions?` | `ReactNode` |  | Buttons. Put the primary action last in the DOM: it sits at the end of the row and on top when the row stacks. |
| `onRetry?` | `() => (void \| Promise<unknown>)` |  | Adds an outline retry button after the actions (meant for the error and offline presets). A returned promise keeps it loading until it settles. |
| `retryLabel?` | `string` | `"Try again"` | The retry button's label. |
| `titleAs?` | `"h2" \| "h3" \| "h4" \| "p"` | `"h3"` | The title's element, to fit the page's heading outline. |
| `live?` | `boolean` | `false` | role="status" with a polite live region, so a screen reader announces the state when it appears. Use it for results that change, like no-results under a search. |
| `animated?` | `boolean` | `true` | Tile, text and actions rise in with a short stagger when the state mounts. Server-rendered markup shows the final state; reduced motion fades only. The demo's replay button remounts it. |
| `presets?` | `Partial<Record<EmptyStatePreset, Partial<EmptyStatePresetConfig>>>` |  | Overrides merged over DEFAULT_EMPTY_STATE_PRESETS ({ icon, tone, title, description, badge } per preset). Translate once for the whole app with <UIProvider defaults={{ "empty-state": { presets } }}>. |
| `children?` | `ReactNode` |  | Extra content between the description and the actions: suggestions, a search field, a list of shortcuts. |
| `classNames?` | `Partial<Record<"media" \| "icon" \| "badge" \| "title" \| "description" \| "content" \| "actions", string>>` |  | Extra classes for the inner parts: the tile or illustration, the glyph, the corner badge, the title, the description, the children wrapper, the actions row. |
| `className?` | `string` |  | Extra classes for the root, merged last. |

## Parts

### EmptyStateIcon (component)

The decorative icon tile on its own (data-slot empty-state-media). Inside an EmptyState it follows its size and entrance. Props: { tone?, color?, badge?, size?, classNames?: { icon, badge } } & div props.

### EmptyStateTitle (component)

The title part, sized by the surrounding EmptyState. Props: { as?: "h2" | "h3" | "h4" | "p" } & heading props.

### EmptyStateDescription (component)

The description part, sized by the surrounding EmptyState. Takes p props.

### EmptyStateActions (component)

The actions row (wraps, stacks full width below 400px). asChild renders your own element as the row. Props: { asChild?: boolean } & div props.

### DEFAULT_EMPTY_STATE_PRESETS (constant)

The English preset copy, icons and tones, to spread and translate. Type: Record<EmptyStatePreset, EmptyStatePresetConfig>.

## Examples

- **All six presets**: Each preset brings its icon, tone and copy; the demo passes its own copy through presets.
- **No results in a search**: Type a city: the list and the empty state swap with a fade, and the clear action empties the query.
- **Error with retry**: onRetry returns a promise, so the button spins. The first retry fails, the second loads.
- **First run with an illustration**: dashed, lg, brand tone and a fanned photo stack in place of the tile.
- **Start aligned in a card**: align="start" for sidebars and widgets, with a custom icon.
- **Inside a table**: size="sm" in a full-width cell under a real header.
- **Sizes and a custom color**: sm, md and lg side by side, tinted with color="#7C3AED".

## See also

- [Alert](https://wingo-ui.com/components/alert) ([Markdown](https://wingo-ui.com/components/alert.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [List](https://wingo-ui.com/components/list) ([Markdown](https://wingo-ui.com/components/list.md))
- [Card](https://wingo-ui.com/components/card) ([Markdown](https://wingo-ui.com/components/card.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))

---

Canonical: https://wingo-ui.com/components/empty-state
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
