Empty state
Empty state — the panel that stands in for a list with nothing in it: a decorative mark, a heading, one line of explanation, and usually one action.
Examples
No invoices yet
Invoices you create appear here, newest first.
<div class="empty">
<h3 class="empty-title">No invoices yet</h3>
<p class="empty-description">Invoices you create appear here, newest first.</p>
<div class="empty-actions">
<button class="btn btn-primary" type="button">New invoice</button>
</div>
</div>A heading, a sentence, and the one action that ends the state — that is the whole component, and there is deliberately nothing above it.
.empty-media is still there if your product has art or a glyph worth showing, and --empty-media-size sizes it; the default examples leave it out because on a component whose job is to be quiet, the picture is the largest thing on the screen and the first thing you stop reading past.
No results for “kingfisher”
Try a shorter word, or clear the filter.
<div class="empty">
<h3 class="empty-title">No results for “kingfisher”</h3>
<p class="empty-description">Try a shorter word, or clear the filter.</p>
<div class="empty-actions">
<button class="btn" type="button">Clear filter</button>
</div>
</div>Two empty states that look alike and are not: "you have not made one yet" offers the action that creates one, and "your filter matched nothing" offers the action that clears the filter.
Getting them the wrong way round is the usual failure — a "Create invoice" button on a search that found nothing is an answer to a question nobody asked. An empty state is never a live region: it is what the region contains, not an announcement.
Tokens 17
Level 2, declared on .empty itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --empty-actions-gap | var(--space-2) |
| --empty-description-color | var(--color-on-surface-muted) |
| --empty-description-font-size | var(--font-size-sm) |
| --empty-description-measure | var(--measure-narrow) |
| --empty-gap | var(--space-3) |
| --empty-media-color | var(--color-on-surface-subtle) |
| --empty-media-size | 2.5rem |
| --empty-padding-block | var(--space-12) |
| --empty-padding-inline | var(--space-6) |
| --empty-title-color | var(--color-on-surface) |
| --empty-title-font-size | var(--font-size-lg) |
| --empty-bg | transparent |
| --empty-border-color | transparent |
| --empty-border-style | solid |
| --empty-border-width | var(--border-width) |
| --empty-color | var(--color-on-surface) |
| --empty-radius | var(--radius-box) |
Variants and states
Variants
.empty-boxed.empty-error.empty-lg.empty-sm
Inside it
.empty-media.empty-actions.empty-description.empty-title
State it reads
None.
Before you ship it
What you have to do 4 requirements
- Put
.empty-titleon a real heading at the level the page's outline calls for. See above for each of the three ways to get it wrong. - Put
aria-hidden="true"on the illustration. Nothing in CSS can add it and nothing can detect its absence. - Put
role="status"on the container that swaps, not on.empty, and have that container in the page before the first search. - Use
.empty-errorfor a load failure and nothing else. It is the one case where the panel means something is wrong.
src/css/components/empty-state.css · npx mostlycss add empty-state