# Empty state

> A useful next step when there is nothing to show yet.

- Category: Cards
- Import: `import { EmptyState } from "@sagui/ui";`
- Page: /components/empty-state

```tsx
import { Button, EmptyState } from "@sagui/ui";

export function Hero() {
  return (
    <div className="w-full max-w-md rounded-[var(--radius-xl)] border border-dashed border-border">
      <EmptyState title="No projects yet" description="Create your first project to start collaborating with your team." action={<Button>New project</Button>} />
    </div>
  );
}
```

## When to use

- Empty lists, zero search results, and first-run views.
- A view that should morph between states, such as empty and success, by changing props.

## When not to use

- Use skeleton while data is still loading.
- Use alert for errors inside a page that still has content.
- Use onboarding-checklist when first-run needs several steps.

## Installation

Install the package and import the styles once. The [installation guide](/docs/installation) covers the Tailwind setup.

```bash
npm install @sagui/ui
```

## Usage

```tsx
import { Search } from "lucide-react";
import { Button } from "@sagui/ui";
import { EmptyState } from "@sagui/ui";

export function NoResults({ onClear }: { onClear: () => void }) {
  return (
    <EmptyState
      icon={<Search size={24} />}
      title="No matches"
      description="Try a shorter search or clear the filters."
      action={<Button variant="secondary" onClick={onClear}>Clear filters</Button>}
    />
  );
}
```

## Examples

### No results

```tsx
import { Search } from "lucide-react";
import { Button, EmptyState } from "@sagui/ui";

export function NoResults() {
  return (
    <div className="w-full max-w-md rounded-[var(--radius-xl)] border border-dashed border-border">
      <EmptyState
        icon={<Search size={24} />}
        title="No results for “atlas”"
        description="Try a shorter word or check the spelling."
        action={<Button variant="secondary">Clear search</Button>}
      />
    </div>
  );
}
```

### Morphing between states

Keep the component mounted and change its props. The icon crossfades, the copy rises in, and the height springs to fit.

```tsx
import { useState } from "react";
import { Inbox } from "lucide-react";
import { Button, EmptyState } from "@sagui/ui";

export function Morphing() {
  const [caughtUp, setCaughtUp] = useState(false);
  return (
    <div className="w-full max-w-md rounded-[var(--radius-xl)] border border-dashed border-border">
      <EmptyState
        icon={caughtUp ? <Inbox size={24} /> : undefined}
        title={caughtUp ? "You are all caught up" : "No projects yet"}
        description={caughtUp ? "New activity will show up here." : "Create your first project to start collaborating."}
        action={<Button variant="outline" onClick={() => setCaughtUp((value) => !value)}>Toggle state</Button>}
      />
    </div>
  );
}
```

## API reference

### EmptyState

A centered icon, title, description, and optional action for empty views.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` (required) | `string` | – | Headline. A new title rises in while the old one leaves. |
| `description` (required) | `string` | – | One or two sentences on why it is empty and what to do. |
| `action` | `ReactNode` | – | Call to action, usually a button. |
| `icon` | `ReactNode` | `<Folder />` | Leading icon. A different icon component crossfades in. |
| `className` | `string` | – | Class for the root section. |
| `label` | `string` | – | Accessible name for the section region. |

## Accessibility

- Renders a section with an h3 title; pass label to name the region.
- The icon is aria-hidden.
- Outgoing copy is hidden from assistive tech while it fades.

## Motion

- Changing title or description rolls the copy in place while the block height springs to fit.
- A new icon pops in with a short blur.
- Reduced motion swaps copy with a fade and snaps height; the icon's idle animation stops.

## Responsive behavior

- Padding scales with the viewport between fixed bounds, and the description caps at 18rem so lines stay short.
- Actions wrap and center, so two buttons stack on narrow screens.

## Performance

- One ResizeObserver drives the height spring; the icon's idle animation is CSS and stops under reduced motion.

## Notes

- Use for empty lists, zero search results, and first-run views. Use skeleton while data is loading.
- Keep it mounted and change its props to morph between states such as empty and success.

## Related

- [Alert](/components/alert): A persistent message that helps people recover or continue.
