Empty state
An empty state is a centered placeholder for “nothing here yet” or “no results”: an optional decorative icon, a title, a description, and an optional action area. CSS-only. See Accessibility for the heading level and decorative-icon guidance.
<div class="re-empty-state">
<div class="re-empty-state__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" focusable="false">
<path
d="M3 8.5 6 4h12l3 4.5M3 8.5V19a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V8.5M3 8.5h6l1.5 3h3L15 8.5h6"
stroke="currentColor"
stroke-width="1.5"
stroke-linejoin="round"
/>
</svg>
</div>
<h3 class="re-empty-state__title">No projects yet</h3>
<p class="re-empty-state__description">Create your first project to start tracking work.</p>
<div class="re-empty-state__actions">
<button class="re-button" data-variant="primary" type="button">New project</button>
<a class="re-button" data-variant="secondary" href="#docs">Learn more</a>
</div>
</div> Compact
Section titled “Compact”data-size="sm" shrinks the padding, icon and type for cards and table cells.
No results
Try a different search term.
<div class="re-empty-state" data-size="sm">
<h3 class="re-empty-state__title">No results</h3>
<p class="re-empty-state__description">Try a different search term.</p>
</div> Bordered drop-zone
Section titled “Bordered drop-zone”data-bordered adds a dashed panel border — handy for upload affordances.
Drop files to upload
or click to browse from your computer.
<div class="re-empty-state" data-bordered>
<div class="re-empty-state__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" focusable="false">
<path
d="M12 16V4m0 0L8 8m4-4 4 4M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<h3 class="re-empty-state__title">Drop files to upload</h3>
<p class="re-empty-state__description">or click to browse from your computer.</p>
</div> In a table
Section titled “In a table”Put .re-empty-state-cell on the <td> so the cell’s own padding and small
table font don’t fight the block.
| Name | Status | Updated |
|---|---|---|
No matching rowsAdjust your filters and try again. |
||
<table class="re-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Updated</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="3" class="re-empty-state-cell">
<div class="re-empty-state" data-size="sm">
<h3 class="re-empty-state__title">No matching rows</h3>
<p class="re-empty-state__description">Adjust your filters and try again.</p>
<div class="re-empty-state__actions">
<button class="re-button" data-variant="ghost" type="button">
Clear filters
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table> - Vertical rhythm is
gap-only, so omitting the icon or actions reflows cleanly.
Accessibility
Section titled “Accessibility”The empty state is CSS-only — a passive container with no interactive behavior of its own. Any keyboard, focus, and announcement semantics come from the native elements you place inside it.
- Semantics — Author the title as a real heading at the level that fits the
page outline (
<h2>–<h4>); the component never forces one. The action area holds native.re-buttoncontrols (<button>/<a href>), so they keep their native focus order and:focus-visiblering — see the button page. - Decorative icon — The
.re-empty-state__iconslot is mute to assistive tech: mark itaria-hidden="true"and setfocusable="false"on the inner<svg>(as the examples do) so it’s skipped and never tab-focusable. - Async announcement — To announce an empty state that appears after a load
or filter, wrap it in a
role="status" aria-live="polite"region that already exists in the DOM before the content swaps in; injecting the live region together with the message won’t reliably announce.
See the accessibility guide for the project-wide stance.