# Empty States

Explain why a region has nothing to show, and offer the next step that fills it.

## Overview

A region can be empty for four reasons, and each one calls for different words and actions. Telling them apart keeps you from reading a failed request as an empty account, or a filter with no matches as missing data.

| Variant     | Cause                              | Example                  | Next step             |
| ----------- | ---------------------------------- | ------------------------ | --------------------- |
| First use   | Nothing exists yet                 | No activity yet          | Add funds             |
| No results  | A search or filter matched nothing | No contacts match “zara” | Clear search          |
| Cleared     | You finished or removed everything | No pending requests      | View history, or none |
| Error-empty | The request for the content failed | Couldn't load activity   | Try again             |

## Components

| Component           | Role                                                          |
| ------------------- | ------------------------------------------------------------- |
| `Empty.Root`        | Centers the state in the region it replaces.                  |
| `Empty.Icon`        | A decorative icon in a round badge. It sets `aria-hidden`.    |
| `Empty.Title`       | States what is missing, as an `h3`.                           |
| `Empty.Description` | Explains why and what happens next, wrapping at 24rem.        |
| `Empty.Actions`     | Holds the button that fills the region, in a centered row.    |
| `Button`            | The next step, such as Add funds, Clear search, or Try again. |
| `Combobox.Empty`    | The same message inside a combobox popup, such as No results. |

## Structure

1. The region's heading, such as the `Surface.Title` above it, so you still know where you are.
2. An icon that hints at the content, such as `History` for activity or `Key` for API keys.
3. A title that names what is missing, then a description that says why and what to do.
4. One primary action that resolves the state, plus a secondary action when it helps.

## Variants

### First Use

```tsx
import { Button, Empty, style, Surface } from '@tempoxyz/ds'
import { History } from '@tempoxyz/ds/icons'

export const fill = true
export function Demo() {
  return (
    <div {...styles.page()}>
      <Surface {...styles.card()}>
        <Surface.Header>
          <Surface.Title>Activity</Surface.Title>
        </Surface.Header>
        <Empty.Root>
          <Empty.Icon>
            <History />
          </Empty.Icon>
          <Empty.Title>No activity yet</Empty.Title>
          <Empty.Description>
            Payments you send and receive appear here. Add funds to make your first payment.
          </Empty.Description>
          <Empty.Actions>
            <Button size="small">Add funds</Button>
          </Empty.Actions>
        </Empty.Root>
      </Surface>
    </div>
  )
}

namespace styles {
  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '26rem !custom',
    width: '100% !custom',
  })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    justifyItems: 'center',
    padding: '4',
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/EmptyFirstUse.tsx)

The first time you open a view, say what will appear here and how to start. The action is the task that creates the first item, such as Add funds or Create key.

### No Results

```tsx
import { Button, Empty, Field, Identicon, Input, Rows, style, Surface } from '@tempoxyz/ds'
import { Search } from '@tempoxyz/ds/icons'
import { useRef, useState } from 'react'

export const fill = true
const contacts = [
  ['Ada', '0x71C7656EC7ab88b098defB751B7401B5f6d8976F'],
  ['Payroll', '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC'],
  ['Treasury', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'],
] as const

export function Demo() {
  const input = useRef<HTMLInputElement>(null)
  const [query, setQuery] = useState('zara')
  const matches = contacts.filter(([name, address]) =>
    `${name} ${address}`.toLowerCase().includes(query.trim().toLowerCase()),
  )

  return (
    <div {...styles.page()}>
      <Surface {...styles.card()}>
        <Surface.Header>
          <Surface.Title>Contacts</Surface.Title>
        </Surface.Header>
        <Field.Root>
          <Input.Group size="small">
            <Input.Prefix>
              <Search />
            </Input.Prefix>
            <Input.Control
              aria-label="Search contacts"
              onValueChange={setQuery}
              placeholder="Name or address"
              ref={input}
              type="search"
              value={query}
            />
            <Input.Clear />
          </Input.Group>
        </Field.Root>
        {/* Stays mounted, so screen readers hear the count change as you type. */}
        <p role="status" {...styles.status()}>
          {matches.length === 1 ? '1 contact' : `${matches.length} contacts`}
        </p>
        {matches.length > 0 ? (
          <div {...styles.rows()}>
            <Rows.Root dividers="full">
              {matches.map(([name, address]) => (
                <Rows.Row key={address} spacing="none">
                  <Identicon address={address} aria-hidden size="24" />
                  <Rows.Info>
                    <Rows.Title>{name}</Rows.Title>
                    <Rows.Description>
                      {address.slice(0, 6)}…{address.slice(-4)}
                    </Rows.Description>
                  </Rows.Info>
                </Rows.Row>
              ))}
            </Rows.Root>
          </div>
        ) : (
          <Empty.Root>
            <Empty.Icon>
              <Search />
            </Empty.Icon>
            <Empty.Title>No contacts match “{query.trim()}”</Empty.Title>
            <Empty.Description>
              Check the spelling, or search by the full 0x address.
            </Empty.Description>
            <Empty.Actions>
              <Button
                onClick={() => {
                  setQuery('')
                  input.current?.focus()
                }}
                size="small"
                variant="secondary"
              >
                Clear search
              </Button>
            </Empty.Actions>
          </Empty.Root>
        )}
      </Surface>
    </div>
  )
}

namespace styles {
  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '26rem !custom',
    width: '100% !custom',
  })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    justifyItems: 'center',
    padding: '4',
  })

  export const rows = style({ marginBlockStart: '2' })

  export const status = style({
    clipPath: 'inset(50%) !custom',
    height: '1px !custom',
    margin: '0',
    overflow: 'hidden',
    position: 'absolute',
    whiteSpace: 'nowrap',
    width: '1px !custom',
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/EmptyNoResults.tsx)

Repeat the query in the title so you can spot a typo, and offer Clear search or Clear filters as a `secondary` button. Type a name such as Ada in the example to see matches return.

Keep the search field and filters visible above the empty state, so you can change them without starting over.

### Cleared

```tsx
import { Button, Empty, style, Surface } from '@tempoxyz/ds'
import { CheckCircle } from '@tempoxyz/ds/icons'

export const fill = true
export function Demo() {
  return (
    <div {...styles.page()}>
      <Surface {...styles.card()}>
        <Surface.Header>
          <Surface.Title>Payment requests</Surface.Title>
        </Surface.Header>
        <Empty.Root>
          <Empty.Icon>
            <CheckCircle />
          </Empty.Icon>
          <Empty.Title>No pending requests</Empty.Title>
          <Empty.Description>
            You've paid or declined every request. New requests appear here.
          </Empty.Description>
          <Empty.Actions>
            <Button size="small" variant="secondary">
              View history
            </Button>
          </Empty.Actions>
        </Empty.Root>
      </Surface>
    </div>
  )
}

namespace styles {
  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '26rem !custom',
    width: '100% !custom',
  })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    justifyItems: 'center',
    padding: '4',
  })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/EmptyCleared.tsx)

When you've processed everything, such as every payment request, confirm that nothing is left. Skip the primary action, since there's no task to start. Link to the history when one exists.

### Error-Empty

```tsx
import { Button, Empty, Rows, style, Surface } from '@tempoxyz/ds'
import { AlertCircle } from '@tempoxyz/ds/icons'
import { useState } from 'react'

export const fill = true
function Failed(props: { onRetry: () => void; retrying: boolean }) {
  return (
    <Empty.Root>
      <Empty.Icon>
        <AlertCircle />
      </Empty.Icon>
      <Empty.Title>Couldn't load activity</Empty.Title>
      <Empty.Description>
        Check your connection, then try again. Your balance and payments aren't affected.
      </Empty.Description>
      <Empty.Actions>
        <Button loading={props.retrying} onClick={props.onRetry} size="small">
          Try again
        </Button>
      </Empty.Actions>
    </Empty.Root>
  )
}

export function Demo() {
  const [loaded, setLoaded] = useState(false)
  const [retrying, setRetrying] = useState(false)

  return (
    <div {...styles.page()}>
      <Surface {...styles.card()}>
        <Surface.Header>
          <Surface.Title>Activity</Surface.Title>
        </Surface.Header>
        {loaded ? (
          <div {...styles.rows()}>
            <Rows.Root dividers="full">
              <Rows.Row spacing="none">
                <Rows.Label>Received from 0x3C44…93BC</Rows.Label>
                120.00 USDC
              </Rows.Row>
              <Rows.Row spacing="none">
                <Rows.Label>Sent to 0x71C7…976F</Rows.Label>
                25.00 USDC
              </Rows.Row>
            </Rows.Root>
          </div>
        ) : (
          <Failed
            onRetry={() => {
              setRetrying(true)
              setTimeout(() => {
                setRetrying(false)
                setLoaded(true)
              }, 1000)
            }}
            retrying={retrying}
          />
        )}
      </Surface>
    </div>
  )
}

namespace styles {
  export const card = style({
    boxSizing: 'border-box',
    maxWidth: '26rem !custom',
    width: '100% !custom',
  })

  export const page = style({
    backgroundColor: 'background.primary',
    display: 'grid',
    justifyItems: 'center',
    padding: '4',
  })

  export const rows = style({ marginBlockStart: '2' })
}
```

[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Patterns/EmptyError.tsx)

When the request for the region fails, say Couldn't load followed by the content, and offer Try again. Set `loading` on the button while the retry runs. See [Feedback and Status](/docs/patterns/feedback-and-status) for the Couldn't load state.

These are reference layouts built from `Surface`, `Empty`, and `Button`, not shipped components.

## Guidelines

### Explain the Cause and the Next Step

Write the title as a short statement of what is missing, such as No API keys yet. Use the description to say why it's empty and what the action does.

**Do:** Say what is missing and give the action that adds it.

**Don't:** Show a bare No data message with nothing to do next.

### Never Show a Failed Request as Empty

An empty list after a failed request tells you that you have no activity, which is false. Show the error-empty variant instead, and keep empty states for requests that succeeded with no content.

**Do:** Say the content couldn't load, and offer Try again.

**Don't:** Show No activity yet when the request for activity failed.

### Offer One Primary Action

Give the state one `primary` button, the task that fills the region. Add a `secondary` button only for a related step, such as reading a guide. Use `Button.Link` when the action opens another page.

**Do:** Pair one primary action with an optional secondary one.

**Don't:** Offer several primary actions that compete for attention.

### Fit the State to the Region

* Use the full `Empty` with an icon for a page or a card, such as an empty activity list.
* In a small region, such as a table filtered to zero rows or a `Popover`, use a single line of text without the icon.
* In a `Combobox`, render `Combobox.Empty`, so an empty result is announced instead of showing an empty popup.
* In a `Table`, put the empty state below the header row, so the columns stay visible.

### Don't Show an Empty State While Loading

Show a `Skeleton` or `Spinner` until the request settles, then decide between content, an empty state, and an error. An empty state that flashes before data arrives reads as a missing balance. See [Loading](/docs/patterns/loading).

## States

| State    | What you show                                                                                            |
| -------- | -------------------------------------------------------------------------------------------------------- |
| Default  | The region's content. The empty state replaces it only when the content is missing.                      |
| Loading  | A placeholder, never the empty state.                                                                    |
| Error    | The error-empty variant with Try again, when nothing loaded at all.                                      |
| Empty    | The first use, no results, or cleared variant, chosen by the cause.                                      |
| Disabled | When you can't take the action, such as without permission to create keys, leave it out and say who can. |
| Success  | Once you take the action, the new item replaces the empty state, and a `Toast` confirms it.              |

## Accessibility

* The `Empty` parts render plain elements in the order you compose them, with no live region.
* `Empty.Title` renders an `h3`, so place the state under an `h2`, such as a `Surface.Title`.
* `Empty.Icon` sets `aria-hidden`, so the title and description must carry the meaning.
* When results change as you type, announce the count through a `role="status"` region that stays mounted, such as 0 contacts.
* Move focus to the search field after Clear search, so you can type a new query.

## Related

* [Empty](/docs/components/empty) for the component parts
* [Combobox](/docs/components/combobox) for empty popups
* [Loading](/docs/patterns/loading) for what shows before content
* [Feedback and Status](/docs/patterns/feedback-and-status) for the Couldn't load state
* [Tables and Lists](/docs/patterns/tables-and-lists) for empty tables
