# Empty state

A lit, dithered illustration with a title, a description and an action, for views with nothing in them yet.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/empty-state.json](https://dithercn.dev/r/empty-state.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/empty-state.json
```

## Usage [#usage]

```tsx
import { EmptyState } from "@/components/dithercn/empty-state";
import { Button } from "@/components/ui/button";

export function NoProjects() {
  return (
    <EmptyState
      illustration="folder"
      title="No projects yet"
      description="Create a project to start building."
      action={<Button>New project</Button>}
    />
  );
}
```

Above the text sits a small lit object drawn in dither cells. Each surface takes a tone from a light up and to the left, so faces turned to the light are dense, faces turned away are sparse, and curved surfaces step through every Bayer level. A one-cell seam splits the object's parts, a clear margin outlines it, and a faint halo and floor shadow sit behind it.

* `box`: an open box with its flaps folded back. The default, for lists and orders.
* `search`: a magnifying glass with a glint on the glass. For search and filters with no results.
* `folder`: a folder with a sheet peeking out. For files, projects and documents.
* `planet`: a lit sphere with a tilted ring. For feeds, events and anything still to arrive.

`matrix` changes the pattern: `bayer4` for crisp steps, `bayer8` for finer ones, `blue-noise` for an even grain.

## Examples [#examples]

### Illustrations [#illustrations]

The four objects in four colours.

```tsx title="examples/empty-state-illustrations.tsx"
import { EmptyState } from "@/components/dithercn/empty-state";
import type { EmptyStateIllustration } from "@/components/dithercn/empty-state";
import type { DitherColorName } from "@/lib/dither";

const SCENES: readonly {
  illustration: EmptyStateIllustration;
  color: DitherColorName;
  title: string;
}[] = [
  { color: "ink", illustration: "box", title: "No orders yet" },
  { color: "grey", illustration: "search", title: "No matches" },
  { color: "orange", illustration: "folder", title: "This folder is empty" },
  { color: "purple", illustration: "planet", title: "Nothing in orbit" },
];

export default function EmptyStateIllustrations() {
  return (
    <div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
      {SCENES.map(({ illustration, color, title }) => (
        <EmptyState
          key={illustration}
          illustration={illustration}
          color={color}
          title={title}
          titleAs="h4"
          className="bg-background ring-border rounded-xl px-4 py-6 ring-1"
        />
      ))}
    </div>
  );
}
```

### Inside a settings panel [#inside-a-settings-panel]

A first-run state for API keys, with the green folder and a dither button for the action.

```tsx title="examples/empty-state-panel.tsx"
import { DitherButton } from "@/components/dithercn/dither-button";
import { EmptyState } from "@/components/dithercn/empty-state";

export default function EmptyStatePanel() {
  return (
    <div className="bg-background ring-border w-full max-w-xl overflow-hidden rounded-xl ring-1">
      <div className="border-border flex items-center justify-between border-b px-5 py-3">
        <span className="text-sm font-medium">API keys</span>
        <span className="text-muted-foreground font-mono text-xs">
          0 active
        </span>
      </div>
      <EmptyState
        color="green"
        illustration="folder"
        title="No API keys"
        description="Create a key to call the API from your server. Keys are shown once, so store them somewhere safe."
        action={
          <DitherButton size="sm" color="green">
            Create key
          </DitherButton>
        }
      />
    </div>
  );
}
```

### No search results [#no-search-results]

The magnifier in grey, for a state the user will see often. Keep these quiet and give a way out.

```tsx title="examples/empty-state-search.tsx"
import { Search01Icon } from "@hugeicons/core-free-icons";

import { DitherButton } from "@/components/dithercn/dither-button";
import { EmptyState } from "@/components/dithercn/empty-state";
import { Icon } from "@/components/icon";

export default function EmptyStateSearch() {
  return (
    <div className="bg-background ring-border w-full max-w-md overflow-hidden rounded-xl ring-1">
      <div className="border-border flex items-center gap-2 border-b px-4 py-3">
        <Icon icon={Search01Icon} className="text-muted-foreground size-4" />
        <span className="font-mono text-sm">ditehr</span>
      </div>
      <EmptyState
        illustration="search"
        color="grey"
        title="No results for “ditehr”"
        description="Check the spelling, or search for a shorter phrase."
        action={
          <DitherButton size="sm" variant="outline" color="grey">
            Clear search
          </DitherButton>
        }
        className="py-8"
      />
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` (required) | `ReactNode` |  | The heading. Say what is empty, plainly. |
| `description` | `ReactNode` |  | One or two sentences: why it is empty and what to do next. |
| `action` | `ReactNode` |  | Buttons or links, under the text. |
| `illustration` | `"box" \| "search" \| "folder" \| "planet"` | `"box"` | The dithered object above the text. |
| `color` | `DitherColor` | `"ink"` | Colour of the illustration: a palette name, a CSS variable name or any CSS colour. |
| `matrix` | `"bayer2" \| "bayer4" \| "bayer8" \| "blue-noise"` | `"bayer4"` | The threshold pattern. |
| `animate` | `boolean` | `true` | Bob the object up and down by one cell. |
| `titleAs` | `"h2" \| "h3" \| "h4"` | `"h3"` | Heading level of the title. |

## Notes [#notes]

* The illustration is decorative and hidden from assistive technology. The title is a real heading; set `titleAs` to fit the page outline.
* The bob moves the object one whole cell in steps, so the dither never smears, and it stops under reduced motion. Each illustration is two SVG paths, computed once per illustration and matrix and cached, so it costs no JavaScript at runtime and renders on the server.
* The illustration keeps a 112 by 72 cell aspect ratio up to 336 pixels wide, so cells are 3 pixels at full size, and it shrinks on narrow screens.