<!-- Protocore Design System — EmptyState -->
# EmptyState

- **Category:** Feedback (`feedback`)
- **Slug:** `feedback/empty-state`
- **Status:** stable
- **Platforms:** web, mobile
- **Import:** `import { EmptyState } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/feedback/empty-state

> A centered, dashed-frame placeholder for a surface that legitimately has nothing to show.

## The five-state doctrine

**EmptyState** is state 2 of the five every data surface must render — *loading, empty, error, pending, data*. It means the request **succeeded** and the answer is genuinely nothing.

- Still fetching? That's **loading** — a [LoadingSkeleton](/feedback/loading-skeleton), not an empty state.
- Request **failed**? That's **error** — an [ErrorState](/feedback/error-state). Never show "No data" when you actually failed to fetch it; it hides the problem and blocks retry.
- Mutation over existing rows? That's **pending** — keep the data, layer a [Spinner](/feedback/spinner).

Getting this distinction right is the whole point of the doctrine: an empty state is an answer, not an absence of one.

## Mobile (React Native)

**Preview.** `@protocore/pds-mobile` ships the React Native sibling of **EmptyState**. It mirrors the web API where React Native allows; the package is a **preview** with no device-level QA yet, so pin it and expect small changes.

Import it from the mobile package (not `@protocore/pds`), inside a `<PdsProvider>` — there is no stylesheet, so `style` (a `ViewStyle`) replaces `className` and every value comes from the theme:

```tsx
import { EmptyState } from "@protocore/pds-mobile";
```

**Parity with web.** A centered "no data" panel inside a dashed hairline frame.

- Slots: `icon`, a mono uppercase `eyebrow` (default `NO DATA`), `title`, `description`, and an `action` (e.g. a Button).

```tsx
<EmptyState
  eyebrow="NO DEPLOYS"
  title="Nothing shipped yet"
  description="Your deployments will show up here."
  action={<Button>Deploy service</Button>}
/>
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `action` | `ReactNode` | no | — | Action slot (e.g. a Button) shown below the body. |
| `className` | `string` | no | — | — |
| `description` | `ReactNode` | no | — | Secondary explanatory copy. |
| `eyebrow` | `ReactNode` | no | `NO DATA` | Mono uppercase eyebrow above the title. |
| `icon` | `ReactNode` | no | — | Optional decorative icon slot, rendered above the eyebrow. |
| `style` | `CSSProperties` | no | — | — |
| `title` | `ReactNode` | no | — | Sentence-case headline. |

## Examples

### Basics

A title and a description inside a dashed frame. The `eyebrow` defaults to `NO DATA`; override it to name the specific thing that's missing.

```tsx
import { EmptyState } from "@protocore/pds";

export default function Basics() {
  return (
    <EmptyState
      title="No deployments yet"
      description="Nodes you provision will show up here with their health and region."
    />
  );
}
```

### With icon and action

Add an `icon` for recognition and an `action` (usually a [Button](/inputs/button)) that resolves the emptiness — the single most useful next step.

```tsx
import { Button, EmptyState } from "@protocore/pds";
import { Boxes, Plus } from "lucide-react";

export default function WithAction() {
  return (
    <EmptyState
      icon={<Boxes size={28} strokeWidth={1.5} />}
      eyebrow="NO NODES"
      title="Provision your first node"
      description="Spin up a validator to start earning protocol rewards on the ledger."
      action={<Button startIcon={<Plus size={16} />}>Provision node</Button>}
    />
  );
}
```

## Do & don't

**Do**

- Reserve EmptyState for a successful fetch that returned nothing.
- Offer one clear next action ("Provision node", "Clear filters").
- Distinguish "nothing yet" (invite creation) from "no matches" (offer escape).
- Rewrite the eyebrow and title to name the specific missing thing.

**Don't**

- Use EmptyState to mask a failed request — that's an ErrorState.
- Show it while data is still loading.
- Leave the generic "NO DATA" eyebrow on a user-facing screen.
- Stack multiple competing actions inside one empty state.

## Accessibility

**Notes**

- EmptyState renders as a plain region — it is informational, not an alert, so it is not announced assertively.
- The `icon` slot is decorative and marked `aria-hidden`; meaning must live in the title and description text.
- Any `action` you pass (e.g. a Button) keeps its own semantics and keyboard behavior.
- Ensure the title conveys the state in words, never through the dashed frame or icon alone.

## Related

`error-state`, `loading-skeleton`, `button`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
