Feedback
Empty state
Fills the space where content would be: says why it is empty and offers the next step.
Installation
npx sinaui add empty-stateCopy src/lib/ui/EmptyState.svelte, Icon.svelte and tokens.css. No other dependencies.
Examples
No results
Shown when a search finds nothing. Clearing puts you back in the search box.
No matches for “astrolabe”
Try a shorter word, or search by author instead.
<script lang="ts">
import { Search01Icon } from '@hugeicons/core-free-icons';
import Button from '#lib/ui/Button.svelte';
import EmptyState from '#lib/ui/EmptyState.svelte';
import SearchField from '#lib/ui/SearchField.svelte';
const titles = ['The Canon of Medicine', 'Book of Optics', 'Al-Jabr', 'Tabula Rogeriana'];
let query = $state('astrolabe');
let field = $state<HTMLInputElement>();
const found = $derived(
titles.filter((t) => t.toLowerCase().includes(query.trim().toLowerCase()))
);
</script>
<div class="stack">
<SearchField label="Search the catalog" bind:value={query} bind:element={field} />
{#if found.length}
<ul>
{#each found as t (t)}<li>{t}</li>{/each}
</ul>
{:else}
<EmptyState icon={Search01Icon} title="No matches for “{query}”" level={3}>
<p>Try a shorter word, or search by author instead.</p>
{#snippet actions()}
<Button
variant="secondary"
onclick={() => {
query = '';
field?.focus();
}}>Clear search</Button
>
{/snippet}
</EmptyState>
{/if}
</div>
<style>
.stack {
display: grid;
gap: 0.75rem;
inline-size: min(100%, 28rem);
margin-inline: auto;
}
ul {
margin: 0;
padding-inline-start: 1.25rem;
}
</style>Nothing here yet
Before there's anything to show, point to where it comes from.
Your reading list is empty
Save manuscripts from the catalog to read them later, even offline.
<script lang="ts">
import { Books01Icon } from '@hugeicons/core-free-icons';
import Button from '#lib/ui/Button.svelte';
import EmptyState from '#lib/ui/EmptyState.svelte';
</script>
<EmptyState icon={Books01Icon} title="Your reading list is empty" level={3}>
<p>Save manuscripts from the catalog to read them later, even offline.</p>
{#snippet actions()}
<Button variant="secondary">Import a list</Button>
<Button href="#first-use">Browse the catalog</Button>
{/snippet}
</EmptyState>Couldn't load
Say what went wrong in plain words, and offer to try again.
The catalog didn't load
Check your connection and try again. Nothing you saved has been lost.
<script lang="ts">
import { CloudOffIcon } from '@hugeicons/core-free-icons';
import Button from '#lib/ui/Button.svelte';
import EmptyState from '#lib/ui/EmptyState.svelte';
let retrying = $state(false);
function retry() {
retrying = true;
setTimeout(() => (retrying = false), 1200);
}
</script>
<EmptyState icon={CloudOffIcon} title="The catalog didn't load" level={3}>
<p>Check your connection and try again. Nothing you saved has been lost.</p>
{#snippet actions()}
<Button variant="secondary" loading={retrying} onclick={retry}>Try again</Button>
{/snippet}
</EmptyState>Props
| Prop | Type | Default |
|---|---|---|
title | string (required) | |
icon | Hugeicons icon | |
level | 2–6: heading level | 2 |
children | Snippet: the explanation | |
actions | Snippet: buttons or links |
Accessibility
- The title is a real heading, so it can be found by jumping between headings. Set
levelto fit the page. - The icon is decoration and hidden from screen readers; the title says it all.
- When it replaces results, move focus somewhere useful: in the first example, clearing returns to the search box.
- Fades in with reduced motion, without the rise.