The empty state: what a list, table, inbox or search renders when it has nothing to show.
Every app reinvents this anatomy by hand - a piece of media, a title, a
short description, one or two actions. <.empty> standardises it so the
empty states across a product read as one system:
<.empty
title="No results found"
description="No projects match your filters. Try a broader search."
>
<:actions>
<.button size="sm" variant="outline" color="gray">Clear filters</.button>
</:actions>
</.empty>Every part is optional - a title on its own renders fine. Without an
:icon slot a default treatment renders: a muted circle with a dashed
ring around a heroicon glyph, marked aria-hidden because it is
decorative.
Variants
default- centred with generous vertical padding, no border. The page-level empty state.compact- tighter padding and type, sized to sit inside a table frame or a list.card- the floating-panel surface (border + radius), for an empty state that has to hold its own on a busy page.dashed- a dashed border on the gray ramp, the drop-target look. Visual only; it ships no drag-and-drop behaviour.
size (sm/md/lg) scales the media, the type and the spacing of
every variant.
A first-run state with actions
<.empty
variant="dashed"
title="No projects yet"
description="Projects hold your environments, deploys and team access."
>
<:icon><.icon name="hero-folder-plus" class="w-6 h-6 text-gray-400" /></:icon>
<:actions>
<.button size="sm">Create your first project</.button>
<.button size="sm" variant="outline" color="gray">Import from Git</.button>
</:actions>
<.a to="/docs/projects" class="text-sm">Learn more about projects</.a>
</.empty>Inside a data table
<.data_table> takes an :empty slot, so the empty state of a table is
the same component as the empty state of a page:
<.data_table id="orders" rows={@rows} state={@state} path={~p"/orders"}>
<:col :let={row} field={:name}>{row.name}</:col>
<:empty>
<.empty
variant="compact"
size="sm"
title="No orders yet"
description="Orders show up here the moment your first customer checks out."
/>
</:empty>
</.data_table>Accessibility
The root is a plain <div> - no landmark, no live region. An empty state
is static content; announcing that a result set changed is the job of the
thing that changed it. The media is aria-hidden="true", and the title,
description and actions sit in DOM order so a screen reader gets a
coherent read.
Summary
Functions
The empty state: media, title, description, actions, trailing line - every part optional, centred in a column.
Functions
The empty state: media, title, description, actions, trailing line - every part optional, centred in a column.
Attributes
title(:string) - the headline, e.g. "No results found". Defaults tonil.description(:string) - one or two sentences of supporting text under the title. Defaults tonil.variant(:string) - default: centred with generous vertical padding; compact: tighter, for table/list empties; card: wrapped in the floating-panel surface; dashed: dashed border, drop-target look. Defaults to"default". Must be one of"default","compact","card", or"dashed".size(:string) - scales the media, the type and the spacing. Defaults to"md". Must be one of"sm","md", or"lg".class(:any) - CSS class for the outer container. Defaults tonil.- Global attributes are accepted.
Slots
icon- custom icon or illustration; without it a default treatment renders - a muted circle with a dashed ring around a heroicon glyph. The media wrapper is always aria-hidden, so keep this slot decorative - any meaningful text belongs in the title or description.actions- primary and secondary actions (buttons or links) rendered under the description.inner_block- an optional trailing line, e.g. a "Learn more" link rendered under the actions.