sveltelean Primitives
Versionv0.2.0 GitHub

Example

No invoices yet

No invoices yet

Invoices appear here after the first payment.

<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>
Tier 0: the two sources differ only by the stylesheet imports. The empty icon part draws a tray from its pseudo-elements. Everything here works with page JavaScript disabled.

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/styles
stylesheets
import '@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

PartElementdata-slean-partRequiredNotes
root<div data-slean="empty">–yesdata-variant="outline" draws a dashed border. No role of its own.
icon<span aria-hidden="true">iconnoEmpty: a drawn tray. With an <svg>: sized to 1.5rem.
titlea heading, or <p>titlenoWhat is empty, at the heading level that fits the page.
description<p>descriptionnoWhy, and what fills it.
actions<div> of links or buttonsactionsnoCentred; 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

KeyWhenResult
TabanywhereReaches the actions (native); the empty state is not focusable

Platform features

FeatureBaselineOutside the target
Headings, paragraphs, links, buttonsWidely availableNot applicable
:empty with generated contentWidely availableNot 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:

TokenDefault (light)Applies to
--slean-space-20.5remgap, margin-block-end
--slean-space-82.5rempadding-block
--slean-space-61.5rempadding-inline
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-leading1.5line-height
--slean-border-strongvar(--slean-neutral-8)border
--slean-radius-lg0.875remborder-radius
--slean-mutedvar(--slean-neutral-3)background
--slean-fg-mutedvar(--slean-neutral-11)color
--slean-radius-sm0.375remborder-end-start-radius, border-end-end-radius
--slean-text-md1remfont-size
--slean-font-weight-semibold600font-weight
--slean-space-30.75remmargin-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

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.

after a search
<!-- 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.

InvoiceDateAmount

No invoices in September

table cell
<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

Source