flyingsalmon

Empty State

A resting no-content block. It says why a region holds nothing and what the user can do about it, in one look, with no surface of its own.

Not feedback, not loading

An empty state is a resting state. Use it when a page, a list, or a card is legitimately empty. An error that follows something the user did is feedback and belongs to the acting surface's alert, or to a notice when the shell owns the result. A region still fetching shows a skeleton: the skeleton holds the shape of content that is coming, the empty state says content is not coming until the user acts. A dead share link is an empty state, not an error: the user landed on a page with nothing in it, they did not act and fail.

Slots

Five slots. EmptyState renders a section labelled by its title. EmptyStateIcon is optional and holds any node inside a --muted circle. EmptyStateTitle is required and carries the label. EmptyStateDescription and EmptyStateActions are optional. There is no variant prop: one look, and the consumer supplies a boundary by wrapping the block in a Card.

Light

No trips yet

Tell hottrip where you want to go and it drafts the route.

Dark

No trips yet

Tell hottrip where you want to go and it drafts the route.

Two recipes

The block is centered and full width with a min-content height, so it takes its boundary from wherever you put it. Centered in the page it reads as the page's own message; wrapped in a Card it reads as one region's message among others. The component paints no border and no background either way.

Light

This trip link has expired

The owner stopped sharing it, or the trip was deleted.

Activities come after the route

Approve the route and 10 days of activities generate.

Dark

This trip link has expired

The owner stopped sharing it, or the trip was deleted.

Activities come after the route

Approve the route and 10 days of activities generate.

Sizes

Two sizes. Default is for page-level emptiness: a text-lg title above a 48px circle holding a 24px icon. Small is for a block inside a card, where the card already carries the heading weight: a text-base title and a 40px circle holding a 20px icon. The description keeps text-sm in both. Inside the block the gap is gap-2, and the actions row adds mt-2 on top of it, so it sits a full gap-4 step below the text.

Light

No credits yet

Credits pay for route and activity generation.

Dark

No credits yet

Credits pay for route and activity generation.

Actions

One or two buttons, primary first. On narrow widths they stack in that same order, so the primary stays on top. An empty state with no action is normal: the route proposal above has nothing for the user to do until the route is approved, so it explains and stops. The block knows nothing about Button's props — you pass the buttons you want.

Light

No trips yet

Tell hottrip where you want to go and it drafts the route.

Dark

No trips yet

Tell hottrip where you want to go and it drafts the route.

Contrast

The title is --foreground and the description is --muted-foreground, both measured on --background and on --card in light and dark mode. The title runs 18.3:1 in light mode and 15.5:1 at its worst in dark, on --card. The description runs 5.2:1 in light mode, 7.0:1 on --background in dark and 6.3:1 on --card. The only --muted surface inside the block is the icon circle, which holds an icon rather than text at 4.7:1 light and 5.3:1 dark. Never place the block itself on --muted without switching the description to --foreground: the description would land on 4.7:1 light and 5.3:1 dark, which clears AA but leaves nothing to spare.

Accessibility and motion

The block is a region labelled by its title, so a screen reader user can find it and hear what it is. The title renders h2 by default and takes an as prop for the level the surrounding document needs — h3 inside a card. The icon is aria-hidden. There is no role="status" and no live region: a resting state must not announce itself on every render. Tab reaches the action buttons and nothing else. There is no animation, because the state an empty state replaces is usually a skeleton and the swap displaces no siblings.