# Empty State (Notchset): prompt.md (v1.0.0)

- id: `empty-state` · version 1.0.0 · component · free
- category: Feedback
- build: Base UI (one set of files; its dependencies follow the build)
- install (this build): `npx shadcn@latest add https://beautiful-ui.dev/r/empty-state.json`
- npm dependencies: none
- registry dependencies: utils, https://beautiful-ui.dev/r/notchset-foundation.json
- docs: https://beautiful-ui.dev/components/empty-state
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

An empty state that arrives: a crop-mark frame settles, a reticle draws itself, then a condensed title, a line of body and your actions; it fades up and away when content arrives.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: none beyond React.
- Files: `components/ui/notchset/empty-state.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`.
- Builds: one set of files for both, but its dependencies come in Base UI and Radix builds. Install the one that matches the project (see Install): a free item's bare URL installs the Base UI build of it and its dependencies.
- Exports to keep: `EmptyState`, and every exported type.
- CSS: the install adds the notchset foundation (tokens, keyframes, motion levels) to your global stylesheet once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Tokens: retheme with the `--notchset-*` custom properties (`--notchset-check`, `--notchset-control-edge`, `--notchset-draw-from`, `--notchset-ease-bloom`, `--notchset-ease-travel`, `--notchset-fade`, `--notchset-focus-color`, `--notchset-focus-inset`, `--notchset-life-from`, `--notchset-life-ms`, `--notchset-node-blink`, `--notchset-node-bloom`, `--notchset-node-delay`, `--notchset-plate-color`, `--notchset-rise-from`, `--notchset-rule`, `--notchset-scroll`, `--notchset-signal`, `--notchset-sweep-to`). Never add Tailwind colour classes inside the component.

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

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `EmptyState` | `empty-state` | The block. |

Style a part with `[data-slot="<slot>"]` selectors or its `className`; keep the attributes when editing.

## Sound
- Keep every `data-slot` and `data-sound` attribute: the sound layer reads them.
- Installing this item adds no audio. Nothing plays until the app mounts `SoundProvider` once (install: `npx shadcn@latest add https://beautiful-ui.dev/r/notchset-sound.json`, import from `@/components/ui/notchset/sound-provider`); `useSound()` gives `muted` and `setMuted` for a mute control. Without a provider the audio engine never loads.

## Match the original
- Read `components/ui/notchset/empty-state.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when
- empty state, no data, zero state, onboarding, first run, blank slate, Notchset
- A list or view with nothing in it yet

### Not when
- Errors: use Alert

## Mistakes
- Offer one primary action

## Usage

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

export function NoEvals({ onCreate }: { onCreate: () => void }) {
  return (
    <EmptyState title="No evals yet" actions={<Button onClick={onCreate}>Create eval</Button>}>
      Evals score Planner before every deploy.
    </EmptyState>
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `title` | `ReactNode` |  | What's empty. |
| `children` | `ReactNode` |  | What to do about it. |
| `actions` | `ReactNode` |  | Your buttons. |
| `in` | `boolean` |  | False fades it out (inert). |

Full docs: https://beautiful-ui.dev/components/empty-state

## Customising
- Colours: the component reads your shadcn tokens (`--background`, `--foreground`, `--border` …), refined by the `--notchset-*` tokens. The signal colour is `--notchset-signal` (it falls back to `--destructive`). Set tokens on `:root`, or on any container to retheme one area.
- Dark mode follows the `.dark` class on an ancestor (the shadcn and next-themes convention).
- Update later by re-running the install with `--overwrite` (review the diff if you edited it). Changelog: https://beautiful-ui.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| Tab | Your actions |

## Performance

- CSS only.

## Responsive

- Centred; the body wraps at 34ch.

## Motion inventory

| Interaction | What moves |
|---|---|
| Mount | The frame settles from 130% (460ms pop); the ring draws (620ms, +160ms); the node and ticks fade in (+620ms) |
| Leave | Fades and lifts 8px (240ms) |

## Accessibility contract (preserve when editing)
- When hidden it's aria-hidden and inert, so its buttons leave the tab order

## Install

Base UI project (a base-* style in components.json):

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

Radix project (a radix-*, new-york or default style):

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/radix-nova/empty-state.json
```

Or add the `@beautiful-ui` registry to components.json and run `npx shadcn@latest add @beautiful-ui/empty-state`: the CLI picks the build from your style.

## Credits
- Built on shadcn/ui (https://ui.shadcn.com)

## Source (Base UI build)

### components/ui/notchset/empty-state.tsx

```tsx
"use client";

/**
 * Empty State (Notchset) v1.0.0 · Beautiful UI, by Chimpy (https://x.com/stochastichimp)
 * Docs: https://beautiful-ui.dev/components/empty-state · Agent prompt: https://beautiful-ui.dev/md/empty-state.md
 * MIT licensed: free to use, change and share.
 */

import * as React from "react";
import { cn } from "@/lib/utils";
import { NOTCHSET_ROOT } from "@/lib/beautiful-ui/notchset/instrument";

/*
 * Notchset Empty State (handoff 1.18; one file for both builds). A crop-mark frame that settles from
 * 130% (460ms pop), a reticle ring that draws in (620ms, +160ms), its node and ticks fading in after,
 * then a condensed title, a line of body and your actions. Set `in` false to fade it up and away.
 */

const POP = "cubic-bezier(.34,1.9,.5,1)";
const TRACE = "cubic-bezier(.7,0,.25,1)";
const SETTLE = "cubic-bezier(.34,1.3,.5,1)";

function EmptyState({ title, children, actions, in: shown = true, className }: { title: React.ReactNode; children?: React.ReactNode; actions?: React.ReactNode; /** False fades it out and lifts it 8px (it stays mounted, hidden from assistive tech). */ in?: boolean; className?: string }) {
  const [pre, setPre] = React.useState(true);
  React.useEffect(() => {
    const a = requestAnimationFrame(() => requestAnimationFrame(() => setPre(false)));
    return () => cancelAnimationFrame(a);
  }, []);
  const ink = "linear-gradient(var(--foreground),var(--foreground))";
  return (
    <div
      {...NOTCHSET_ROOT}
      data-slot="empty-state"
      aria-hidden={!shown || undefined}
      inert={!shown}
      className={cn("flex flex-col items-center gap-4 text-center text-foreground", className)}
      style={{ opacity: shown ? 1 : 0, transform: `translateY(${shown ? 0 : -8}px)`, pointerEvents: shown ? "auto" : "none", transition: `opacity 240ms linear, transform 460ms ${SETTLE}` }}
    >
      <div className="relative flex size-[76px] items-center justify-center">
        <span
          aria-hidden
          className="absolute inset-0 bg-no-repeat"
          style={{
            background: [`${ink} top left/10px 1px`, `${ink} top left/1px 10px`, `${ink} top right/10px 1px`, `${ink} top right/1px 10px`, `${ink} bottom left/10px 1px`, `${ink} bottom left/1px 10px`, `${ink} bottom right/10px 1px`, `${ink} bottom right/1px 10px`].join(", "),
            backgroundRepeat: "no-repeat",
            transform: `scale(${pre ? 1.3 : 1})`,
            opacity: pre ? 0 : 1,
            transition: pre ? "none" : `transform 460ms ${POP}, opacity 200ms linear`,
          }}
        />
        <svg aria-hidden width="40" height="40" viewBox="0 0 40 40" className="overflow-visible">
          <circle cx="20" cy="20" r="12" fill="none" strokeWidth="1.2" strokeDasharray="80" className="stroke-foreground" style={{ strokeDashoffset: pre ? 80 : 0, transition: pre ? "none" : `stroke-dashoffset 620ms ${TRACE} 160ms` }} />
          <circle cx="20" cy="20" r="3" className="fill-foreground" style={{ opacity: pre ? 0 : 1, transition: pre ? "none" : "opacity 200ms linear 620ms" }} />
          <path d="M20 2 V7 M20 33 V38 M2 20 H7 M33 20 H38" strokeWidth="1" className="stroke-[var(--notchset-control-edge,var(--input))]" style={{ opacity: pre ? 0 : 1, transition: pre ? "none" : "opacity 200ms linear 620ms" }} />
        </svg>
      </div>
      <span className="font-sans text-[32px] leading-[.95] font-extrabold uppercase [font-stretch:62%]">{title}</span>
      {children && <span className="max-w-[34ch] font-sans text-sm leading-[1.55] text-pretty text-muted-foreground">{children}</span>}
      {actions && <div className="mt-1 flex gap-2.5">{actions}</div>}
    </div>
  );
}

export { EmptyState };
```

The notchset foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://beautiful-ui.dev/r/notchset-foundation.json.
