Components Empty state

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

Empty state

The empty state, standardised - media, title, description and actions. Four variants from bare column to dashed drop-target.

The most common empty state in any product: a search that found nothing. compact keeps it inside the result frame instead of taking over the page, and the secondary action is the way back out - never leave someone staring at a dead end.

First run, with a way in

The first-run state earns a card: it is the whole page, so give it the panel surface, a primary CTA that creates the missing thing, and a trailing docs line for people who want to read before they click.

No projects yet

Projects hold your environments, deploys and team access. Create one to get started.

heex
<.empty
variant="card"
size="lg"
title="No projects yet"
description="Projects hold your environments, deploys and team access. Create one to get started."
>
<:icon>
<div class="flex items-center justify-center bg-primary-50 rounded-full size-16 dark:bg-primary-500/10">
<.icon name="hero-folder-plus" class="w-8 h-8 text-primary-600 dark:text-primary-400" />
</div>
</:icon>
<:actions>
<.button size="sm" label="Create your first project" />
<.button size="sm" variant="outline" color="gray" label="Import from Git" />
</:actions>
<.a to="#" class="text-sm">Learn more about projects</.a>
</.empty>
Four treatments

default is the bare centred column, compact tightens it for lists and tables, card wraps it in the panel surface, and dashed is the drop-target look - a visual treatment only, no drag and drop attached.

default

Centred, no border.

compact

Tighter, for lists.

card

The panel surface.

dashed

The drop-target look.

heex
<div class="grid w-full gap-4 sm:grid-cols-2">
<.empty variant="default" size="sm" title="default" description="Centred, no border." />
<.empty variant="compact" size="sm" title="compact" description="Tighter, for lists." />
<.empty variant="card" size="sm" title="card" description="The panel surface." />
<.empty variant="dashed" size="sm" title="dashed" description="The drop-target look." />
</div>
Inside a data table

The data table takes an :empty slot, so a table's empty state is the same component as a page's. compact plus sm sits inside the table frame without blowing it open.

Name Email
heex
<% state = %State{total: 0} %>
<.data_table id="sx-empty-dt" rows={[]} state={state} path="#">
<:col :let={row} field={:name}>{row.name}</:col>
<:col :let={row} field={:email}>{row.email}</: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>
Properties
Attribute Type Default Description
class any nil CSS class for the outer container
description string nil one or two sentences of supporting text under the title
rest global
size string "md" scales the media, the type and the spacing
one of: "sm", "md", "lg"
title string nil the headline, e.g. "No results found"
variant string "default" 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
one of: "default", "compact", "card", "dashed"
:actions slot slot primary and secondary actions (buttons or links) rendered under the description
:icon slot slot 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
:inner_block slot slot an optional trailing line, e.g. a "Learn more" link rendered under the actions