# Empty state

> Communicates that a section or page has no content, with optional media, message, and actions.

Status: stable. Added in Components 2.8.0. Updated in Components 2.16.0, 20 Oct 2026.

Communicates that a section or page has no content. A single `size` token
scales it from a small empty section up to a whole-page empty or 404 screen.

## Import

```tsx
import { EmptyState } from '@oztix/roadie-components/empty-state'
```

## Examples

### Default

The standalone `md` empty state has an IconTile, a message, and one action.

```tsx
<EmptyState>
  <EmptyState.IconTile>
    <TicketIcon weight='duotone' />
  </EmptyState.IconTile>
  <EmptyState.Title>No events yet</EmptyState.Title>
  <EmptyState.Description>
    This collection doesn't have any events listed yet. Check back soon.
  </EmptyState.Description>
  <EmptyState.Actions>
    <Button href='#'>Back to home</Button>
  </EmptyState.Actions>
</EmptyState>
```

### In a section

Use `size='sm'` for an empty state inside a card or panel.

```tsx
<Card>
  <Card.Content>
    <EmptyState size='sm'>
      <EmptyState.IconTile>
        <TicketIcon weight='duotone' />
      </EmptyState.IconTile>
      <EmptyState.Title>No tickets</EmptyState.Title>
      <EmptyState.Description>
        Tickets you buy will appear here.
      </EmptyState.Description>
    </EmptyState>
  </Card.Content>
</Card>
```

### Whole page

Use `size='lg'` with a custom hero illustration for a full-page empty or 404
screen. Raise the heading to `h1` with `render`.

```tsx
<EmptyState size='lg'>
  <EmptyState.Title render={<h1 />}>This page packed up early</EmptyState.Title>
  <EmptyState.Description>
    The link is broken. The event might be over. Let's find you another gig.
  </EmptyState.Description>
  <EmptyState.Actions>
    <Button href='#' intent='accent' emphasis='strong'>
      Explore events
    </Button>
  </EmptyState.Actions>
  <EmptyState.Illustration>
    <img src='/empty-state-404.png' alt='' />
  </EmptyState.Illustration>
</EmptyState>
```

### Sizes

```tsx
{['sm', 'md', 'lg'].map((size) => (
  <EmptyState key={size} size={size}>
    <EmptyState.IconTile>
      <MagnifyingGlassIcon weight='duotone' />
    </EmptyState.IconTile>
    <EmptyState.Title>No events match your search</EmptyState.Title>
    <EmptyState.Description>Try a different search.</EmptyState.Description>
  </EmptyState>
))}
```

### Media by size

The recommended pairing: `sm` → IconTile, `md` → SpotIllustration,
`lg` → hero illustration. The pairing is a convention, not enforced.

```tsx
<EmptyState size='sm'>
  <EmptyState.IconTile>
    <HeartIcon weight='duotone' />
  </EmptyState.IconTile>
  <EmptyState.Title>No favourites yet</EmptyState.Title>
  <EmptyState.Description>
    Save events you love and they'll show up here.
  </EmptyState.Description>
  <EmptyState.Actions>
    <Button href='#'>Browse events</Button>
  </EmptyState.Actions>
</EmptyState>
<EmptyState size='md'>
  <EmptyState.Illustration>
    <NoteMusic />
  </EmptyState.Illustration>
  <EmptyState.Title>No upcoming events</EmptyState.Title>
  <EmptyState.Description>
    New shows will appear here as they're announced.
  </EmptyState.Description>
  <EmptyState.Actions>
    <Button href='#'>Browse events</Button>
  </EmptyState.Actions>
</EmptyState>
```

### Intents

Set `intent` on the root to tint the IconTile and Button together. Omit it to
inherit the palette from an ancestor instead.

```tsx
<EmptyState intent='accent' size='sm'>
  <EmptyState.IconTile>
    <StarIcon weight='duotone' />
  </EmptyState.IconTile>
  <EmptyState.Title>You're all caught up</EmptyState.Title>
  <EmptyState.Description>No new notifications right now.</EmptyState.Description>
  <EmptyState.Actions>
    <Button href='#' emphasis='strong'>
      View activity
    </Button>
  </EmptyState.Actions>
</EmptyState>
<EmptyState intent='danger' size='sm'>
  <EmptyState.IconTile>
    <WarningIcon weight='duotone' />
  </EmptyState.IconTile>
  <EmptyState.Title>We couldn't load your events</EmptyState.Title>
  <EmptyState.Description>
    Check your connection and try again.
  </EmptyState.Description>
  <EmptyState.Actions>
    <Button href='#' emphasis='strong'>
      Retry
    </Button>
  </EmptyState.Actions>
</EmptyState>
```

### Without actions

Title and description only.

```tsx
<EmptyState size='sm'>
  <EmptyState.IconTile>
    <EnvelopeIcon weight='duotone' />
  </EmptyState.IconTile>
  <EmptyState.Title>Inbox zero</EmptyState.Title>
  <EmptyState.Description>You're all caught up.</EmptyState.Description>
</EmptyState>
```

## Guidelines

Write the title, description, and actions with
[Interface copy](https://ticketsolutionsptyltd.github.io/roadie/content/interface-copy.md#empty-states).

### Match the size to its context

**Do**

```tsx
<EmptyState size='sm'>
  <EmptyState.IconTile>
    <TicketIcon weight='duotone' />
  </EmptyState.IconTile>
  <EmptyState.Title>No tickets</EmptyState.Title>
  <EmptyState.Description>
    Tickets you buy will appear here.
  </EmptyState.Description>
</EmptyState>
```

Use `size='sm'` inside a card or panel, `md` for a standalone state, and
`lg` only for a whole-page empty or 404 screen.

**Don’t**

```tsx
<EmptyState size='lg'>
  <EmptyState.Title>No tickets</EmptyState.Title>
  <EmptyState.Description>
    Tickets you buy will appear here.
  </EmptyState.Description>
</EmptyState>
```

Don't drop a whole-page `lg` state into a small section. It overwhelms the
surrounding UI.

### Set the intent once on the root

**Do**

```tsx
<EmptyState intent='danger'>
  <EmptyState.IconTile>…</EmptyState.IconTile>
  <EmptyState.Title>We couldn't load your events</EmptyState.Title>
  <EmptyState.Actions>
    <Button emphasis='strong'>Retry</Button>
  </EmptyState.Actions>
</EmptyState>
```

Pass `intent` on the root (or inherit it from an ancestor) so the tile and
buttons share one palette.

**Don’t**

```tsx
<EmptyState>
  <EmptyState.IconTile intent='danger'>…</EmptyState.IconTile>
  <EmptyState.Actions>
    <Button intent='danger'>Retry</Button>
  </EmptyState.Actions>
</EmptyState>
```

Don't hard-code the same intent on every child. It drifts out of sync.

### Let EmptyState borrow a surface

**Do**

```tsx
<Card>
  <Card.Content>
    <EmptyState size='sm'>…</EmptyState>
  </Card.Content>
</Card>
```

Wrap the empty state in a `Card` when you need a background or border.
EmptyState has no surface of its own.

**Don’t**

```tsx
<EmptyState className='rounded-xl border bg-raised p-6'>…</EmptyState>
```

Don't bolt an ad-hoc surface onto the root with utility classes. Reach for
the `Card` you already have.

## Accessibility

- `EmptyState.Title` renders an `<h2>` by default. Use `render={<h1 />}` (or
  another level) so the heading fits the surrounding document outline.
  A whole-page state usually wants `h1`.
- Decorative media should be hidden from assistive tech: pass `alt=''` on
  `<img>` heroes. `SpotIllustration` sets `aria-hidden` automatically unless
  you give it an `aria-label`. `IconTile` is not hidden by default. Add
  `aria-hidden` to a purely decorative tile. The title and description carry
  the meaning.

## API reference

### EmptyState

```tsx
import { EmptyState } from '@oztix/roadie-components/empty-state'
```

- `size`: `"sm" | "md" | "lg"`. Scales typography, spacing, and media together.
- `intent`: `"neutral" | "brand" | "brand-secondary" | "brand-blue" | "brand-orange" | "brand-purple" | "accent" | "danger" | "success" | "warning" | "info"`. Palette for the empty state. Omit to inherit from an ancestor.

### EmptyState.Actions

No props beyond the standard HTML attributes.

### EmptyState.Description

No props beyond the standard HTML attributes.

### EmptyState.IconTile

- `intent`: `"neutral" | "brand" | "brand-secondary" | "brand-blue" | "brand-orange" | "brand-purple" | "accent" | "danger" | "success" | "warning" | "info" | null`.
- `emphasis`: `"strong" | "normal" | "subtle" | "subtler" | null`.
- `size`: `"xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | null`.
- `shape`: `"circle" | "square" | null`. Defaults to `circle`.

### EmptyState.Illustration

No props beyond the standard HTML attributes.

### EmptyState.Title

- `render`: `RoadieRenderProp`. Change the heading level, e.g. `render={<h1 />}`. Defaults to `<h2>`.

Docs updated 19 Oct 2026.
