Empty State
Centered placeholder for a region with nothing in it, with room for the action that fills it. Upstream calls this component `empty`.
Added 14 October 2026
Two situations, one component
- Nothing created yet — offer the action that creates the first thing. This is the more valuable case: an empty state here is onboarding, not an error.
- Nothing matched — offer the way back. Clear the filters, widen the range. Say which constraint is probably responsible.
If neither applies — the data failed to load — that is a Notice, not an empty state. An empty state says
“there is nothing”, which is a fact about the data; a failure says “we don’t know”, which is a fact about us.
Announce it when it replaces content
This is the one thing that is easy to miss. An empty state appearing after a filter change is a status message: focus stays on the filter control, the result list silently vanishes, and a screen-reader user gets nothing. That is WCAG 4.1.3.
<EmptyState role="status">…</EmptyState>
<hlm-empty-state role="status">…</hlm-empty-state>
The role is not baked in, deliberately: an empty state that is the initial content of a page must not be a live region — it would announce on load, competing with the page title. One prop, two situations, and the choice is yours to make per use. [a11y]
Composition
<EmptyState className="border">
<EmptyStateHeader>
<EmptyStateMedia variant="icon"><FolderPlusIcon /></EmptyStateMedia>
<EmptyStateTitle>No campaigns yet</EmptyStateTitle>
<EmptyStateDescription>Create your first campaign to start booking inventory.</EmptyStateDescription>
</EmptyStateHeader>
<EmptyStateContent className="flex-row justify-center gap-2">
<Button>Create campaign</Button>
<Button variant="outline">Import</Button>
</EmptyStateContent>
</EmptyState>
EmptyStateMediatakesvariant="icon"for the muted rounded well,variant="icon-lg"for a size-16 circular well suited to section-level empty states (a whole tab or overview pane, not a slot in a card), or the default for a bare icon or an illustration.- The
icon-lgwell is deliberately neutral (sametext-foregroundasicon); if a section has an accent colour, tint the icon by setting the text colour on the media —<EmptyStateMedia variant="icon-lg" className="text-brand-interactive">/<hlm-empty-state-media variant="icon-lg" class="text-brand-interactive">— never the well’sbg-mutedfill. The colour must sit on the media, not the icon element: @ng-icons’ unlayered host rule beats colour utilities placed on theng-iconitself, andcurrentColorinheritance works identically in both frameworks. The icon is decorative (both icon libraries mark itaria-hidden), so a neutral dim likeopacity-40is fine too — but dim or tint, never both: 40% opacity drops an accent below visibility on the dark theme’s muted fill. EmptyStateContentstacks by default; addflex-rowfor a pair of buttons side by side.- The container ships a dashed border colour but no border width, so
className="border"opts into the dashed outline and gets the neutral hairline rather thancurrentColor— this package has no globalborder-colorreset. Withoutborderthere is no visible edge and the box is delimited by whitespace, which is the right default inside a card.
Heading level: in Angular hlmEmptyStateTitle is a directive, so put it on the element you want —
<h2 hlmEmptyStateTitle> when the empty state replaces a whole titled region. In React the part renders a
<div>; use your own heading and the title’s classes if the document outline matters there.
Rules
- One primary action, at most one secondary. An empty state with four buttons is a menu.
- The description says what the action will do, not that the list is empty — the title already said that.
- Keep it inside the region it describes, at the region’s size. A full-page empty state for an empty table cell reads as a crash.
- Icons here are decorative; both icon libraries mark them
aria-hiddenby default, so the message must stand on its own text. It does — don’t add anaria-labelto the media. [a11y]
Never
- An empty state for a loading state — use
Skeleton. - An empty state for a failure — use
Noticeand say what failed. role="status"on an empty state that is present on first render.