Primitives Feedback
Empty state
What a list, a table or a search shows when it has nothing: a heading, a message and an optional action, with a decorative icon. It is content with a layout, and nothing more. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
headings,links and buttons,:empty- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
a heading, a paragraph and an optional action
On this page
Example
<div data-slean="empty" data-variant="outline">
<span data-slean-part="icon" aria-hidden="true"></span>
<h3 data-slean-part="title">No invoices yet</h3>
<p data-slean-part="description">Invoices appear here after the first payment.</p>
<div data-slean-part="actions">
<a href="/primitives/empty" data-slean="button">Set up billing</a>
<a href="/primitives/empty" data-slean="button" data-variant="ghost">Read about billing</a>
</div>
</div><script lang="ts">
import '@svelte-lean/styles/button.css';
import '@svelte-lean/styles/empty.css';
</script>
<div data-slean="empty" data-variant="outline">
<span data-slean-part="icon" aria-hidden="true"></span>
<h3 data-slean-part="title">No invoices yet</h3>
<p data-slean-part="description">Invoices appear here after the first payment.</p>
<div data-slean-part="actions">
<a href="/primitives/empty" data-slean="button">Set up billing</a>
<a href="/primitives/empty" data-slean="button" data-variant="ghost">Read about billing</a>
</div>
</div>Why this implementation exists
An empty list is still a place in the page: the reader should learn what would be there, why it is not, and what to do about it. That is a heading, a paragraph and a link or a button. No state and no script are involved; the application renders it when its collection is empty.
empty.css centres the parts in a column, sizes the icon, keeps the message to a readable width and wraps the actions. The contract covers what markup alone cannot decide: the heading level, the icon staying decorative, and how an empty result after a search is announced.
The browser owns
- the heading in the page outline, the paragraph, the link or button action
- hiding the decorative icon (aria-hidden)
Svelte Lean owns
- empty.css: the centred column, the icon circle and its drawn tray, the outline variant
- the contract: heading level, what to say, how a changed result is announced
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/button.css';
import '@svelte-lean/styles/empty.css';Render the empty state where the content would be, with a heading at the level that fits the page outline, or a <p> inside a table cell, a menu or a small panel.
Say why the place is empty and what fills it. Offer an action when there is one: a link to where the content comes from, or a button that starts it.
When the empty state replaces results after a search or a filter, write it into a role="status" region that is already in the page; the empty state itself has no role.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <div data-slean="empty"> | – | yes | data-variant="outline" draws a dashed border. No role of its own. |
| icon | <span aria-hidden="true"> | icon | no | Empty: a drawn tray. With an <svg>: sized to 1.5rem. |
| title | a heading, or <p> | title | no | What is empty, at the heading level that fits the page. |
| description | <p> | description | no | Why, and what fills it. |
| actions | <div> of links or buttons | actions | no | Centred; wraps on narrow widths. |
Runtime profile
Tier 0: the empty state has no behavior module, the Vite plugin maps empty to no module, and the page ships no Svelte Lean JavaScript for it.
Accessibility contract
- The title is a real heading (or a
<p>where a heading does not fit), so the empty section is found by heading navigation. - The icon is
aria-hidden="true"and says nothing the text does not. - Showing an empty state never moves focus; its actions are native links and buttons in the tab order.
- An empty result after a search is announced through a
role="status"region that exists before the search.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | anywhere | Reaches the actions (native); the empty state is not focusable |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
Headings, paragraphs, links, buttons | Widely available | Not applicable |
:empty with generated content | Widely available | Not applicable |
Without JavaScript
Fully functional: the text is markup and the actions are native links or buttons.
Server rendering
Static markup: the server renders the empty state when the collection is empty.
Before hydration
Nothing is attached; the empty state reads the same before and after hydration.
Styling
empty.css centres the parts, draws the icon circle and the default tray, sets the title and description text and the dashed outline variant. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-2 | 0.5rem | gap, margin-block-end |
--slean-space-8 | 2.5rem | padding-block |
--slean-space-6 | 1.5rem | padding-inline |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-leading | 1.5 | line-height |
--slean-border-strong | var(--slean-neutral-8) | border |
--slean-radius-lg | 0.875rem | border-radius |
--slean-muted | var(--slean-neutral-3) | background |
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-radius-sm | 0.375rem | border-end-start-radius, border-end-end-radius |
--slean-text-md | 1rem | font-size |
--slean-font-weight-semibold | 600 | font-weight |
--slean-space-3 | 0.75rem | margin-block-start |
State selectors the stylesheet targets, all from the platform or ARIA: :empty.
Variant attributes: data-variant (outline).
Compatibility notes
Headings, paragraphs, links, buttons, flex layout and generated content on :empty are Baseline widely available.
Examples
After a search
The role="status" region exists before the search runs; writing the empty state
into it is what a screen reader announces. The empty state itself has no role. An icon part with
an SVG replaces the drawn tray, and the title is a <p> because a heading would
be out of place in a results panel.
No results for “harbour”
Check the spelling or search all projects.
<!-- The status region exists before the search runs; writing the empty state into it is
what a screen reader announces. -->
<div role="status">
<div data-slean="empty">
<span data-slean-part="icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24">…</svg>
</span>
<p data-slean-part="title">No results for “harbour”</p>
<p data-slean-part="description">Check the spelling or search all projects.</p>
<div data-slean-part="actions">
<button type="button" data-slean="button" data-variant="outline">Search all projects</button>
</div>
</div>
</div>In a table
One cell spans every column, so the table keeps its headers and a screen reader still knows which table it is in.
| Invoice | Date | Amount |
|---|---|---|
No invoices in September | ||
<table>
<thead>
<tr><th>Invoice</th><th>Date</th><th>Amount</th></tr>
</thead>
<tbody>
<tr>
<td colspan="3">
<div data-slean="empty">
<p data-slean-part="title">No invoices in September</p>
</div>
</td>
</tr>
</tbody>
</table>Testing
apps/playground/tests/primitives/display-extra.spec.tsPlaywright, with page JavaScript enabled and disabled: roles, names, keyboard, geometry, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/empty/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/empty/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/empty.cssthe optional stylesheet