---
title: Empty
description: Nothing, said properly: what is missing, why that is fine, and what to do next - in that order, quietly.
source: https://adamjurek.com/components/empty
---

## Home


Empty is what a screen shows when there is nothing to show: an icon, a title
stating what is missing, one line on why that is fine, and an action for what
to do next, in that order. Reach for it wherever a list, search or page can
legitimately come back with zero results.

<!-- ::start:showcase demo="empty" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

An empty state is the one screen where the interface has the reader's full
attention, so the order is deliberate: the title says what is missing without
apologising for it, the line underneath explains why, and the action is the
one way out rather than a menu of choices. The icon always renders, defaulting
to a generic glyph, because a bare title with nothing above it reads as a
failed render rather than an intentional empty state.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/empty.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/empty.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Loading |
| Files | `empty.tsx` |
| Dependencies | None |
| Also installs | `icon` |
| Tags | empty-state, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Button, Empty } from "@sushindustries/ui";

export function Example() {
	return (
		<Empty
			title="No posts yet"
			icon="note"
			action={<Button variant="ghost">Write one</Button>}
		>
			Drafts stay off the index until they say otherwise.
		</Empty>
	);
}
```

## What you should see

A quiet block, centred: an icon, the title in bold, one line of explanation
under it, and the action last. Nothing here does anything on its own -
`action` is whatever you pass, this component only places it.

## If nothing happens

`Empty` always renders something, including with no `children` and no
`action` - `icon` defaults to `folder-open` rather than disappearing, since a
bare title with no glyph reads as a failed render rather than an intentional
empty state.


## Guides


## The order is the point

Title, then why, then what to do - in that order, because that is the order
a reader actually needs them in. `title` states what is missing without
apologising for it; `children` is the reason it is fine, not a restatement of
the title; `action` is the one way out, not a list of options.

## When not to use it

A genuine error - a failed request, a broken permission - is not an empty
state. `Empty` says "there is nothing here yet, and that is expected"; a
failure needs to say it failed, which this component has no vocabulary for.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `title` | `string` | - | What there is none of, stated plainly. |
| `children?` | `ReactNode` | - | The way out: why it is empty, or what to do about it. |
| `icon?` | `IconName` | `"folder-open"` | The glyph above the title. Always drawn - a bare empty state reads as a failed render. |
| `action?` | `ReactNode` | - | Usually a Button. |

<!-- /generated:api -->

## Notes

`icon` cannot be turned off - there is no `null` or `false` value that
removes it, only a different glyph. A blank empty state with no icon at all
was tried and reads as a failed render rather than a deliberate one, which is
why the prop has a default instead of being optional in effect.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="empty" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Button, Empty } from "@sushindustries/ui";

export function SearchResults({ query, results }: {
	query: string;
	results: readonly { id: string; title: string }[];
}) {
	if (results.length === 0) {
		return (
			<Empty title={`Nothing for "${query}"`} icon="search">
				Try a shorter word, or check the spelling.
			</Empty>
		);
	}

	return (
		<ul className="flex col gap-2">
			{results.map((r) => (
				<li key={r.id}>{r.title}</li>
			))}
		</ul>
	);
}
```

## What this example is not

Not a loading state. This only covers "the search finished and found
nothing" - a request still in flight is `Spinner`'s job, and conflating the
two shows "nothing here" for a fraction of a second on every search.
