Blocks
Notifications
A bell for your header with the unread count on it, opening an inbox. The inbox works on its own page too: grouped by day, filtered to unread or mentions, with read, mute and older pages.
Examples
A bell in the header
Open it, filter, or point at a row for its quick actions. Simulate a new one and watch the count go up.
Notifications
Today
Yesterday
Earlier
<script lang="ts">
import NotificationBell from '#lib/blocks/notifications/NotificationBell.svelte';
import Button from '#lib/ui/Button.svelte';
import { toast } from '#lib/ui/toast/index.js';
import { arrival, sample } from './data';
let items = $state(sample());
const wait = () => new Promise((r) => setTimeout(r, 400));
</script>
<!-- A stand-in app header; in your app, put the bell in your own. -->
<div class="header">
<strong>Bayt al-Ḥikma</strong>
<NotificationBell
bind:items
onopen={(n) => toast(`Opening: ${n.actor.name}`)}
onread={wait}
onmute={wait}
/>
</div>
<Button variant="secondary" size="sm" onclick={() => items.unshift(arrival())}
>Simulate a new notification</Button
>
<style>
.header {
display: flex;
align-items: center;
justify-content: space-between;
inline-size: min(32rem, 100%);
padding: 0.5rem 0.5rem 0.5rem 1rem;
border-radius: calc(var(--ui-radius) * 1.5);
background: var(--ui-surface);
box-shadow: var(--ui-shadow-card);
}
</style>An inbox page
“Fail the next save”, then Mark all as read: the change shows at once, then goes back and says so. Reload to see the loading and error states.
Today
Yesterday
Earlier
<script lang="ts">
import Inbox from '#lib/blocks/notifications/Inbox.svelte';
import Button from '#lib/ui/Button.svelte';
import { toast } from '#lib/ui/toast/index.js';
import { older, sample } from './data';
let items = $state(sample());
let loading = $state(false);
let error = $state<string>();
let failNext = $state(false);
let pages = $state(0);
const wait = (ms = 500) => new Promise((r) => setTimeout(r, ms));
// Stand-ins for your API. "Fail the next save" shows what happens when saving goes wrong.
async function save() {
await wait(400);
if (failNext) {
failNext = false;
throw new Error('offline');
}
}
async function reload(fail = false) {
loading = true;
error = undefined;
await wait(900);
loading = false;
if (fail) error = 'The library’s server didn’t answer. Check your connection and try again.';
else items = sample();
}
</script>
<div class="page">
<Inbox
title="All notifications"
bind:items
{loading}
{error}
onretry={() => reload()}
onopen={(n) => toast(`Opening: ${n.actor.name}`)}
onread={save}
onmute={save}
more={pages < 2}
onmore={async () => {
await wait(700);
items.push(...older());
pages++;
}}
/>
<div class="controls">
<Button variant="secondary" size="sm" onclick={() => (failNext = true)} disabled={failNext}
>{failNext ? 'The next save will fail' : 'Fail the next save'}</Button
>
<Button variant="ghost" size="sm" onclick={() => reload()}>Reload</Button>
<Button variant="ghost" size="sm" onclick={() => reload(true)}>Reload with an error</Button>
</div>
</div>
<style>
.page {
display: grid;
gap: 1rem;
inline-size: min(36rem, 100%);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
</style>Installation
npx sinaui add blocks/notificationsCopy src/lib/blocks/notifications/, with Avatar, Button, EmptyState, Icon, Popover, RollingNumber, Segmented, Skeleton, the toast folder, announce.ts, motion.ts, now.svelte.ts, optimistic.ts, scroll-edges.ts and tokens.css. No dependencies.
Props
NotificationBell takes everything Inbox does, plus a bindable open.
| Prop | Type | Default |
|---|---|---|
items | { id, actor: { name, src? }, text, at, read?, mention?, muted? }[] (bindable, newest first) | required |
title | string | 'Notifications' |
loading | boolean: shows placeholders | false |
error | string: a message, with Try again calling onretry | |
onopen | (item) => void; it's marked read first | |
onread | (ids, read) => Promise | void: save; if it throws, the change goes back | |
onmute | (item, muted) => Promise | void | |
more, onmore | boolean, () => Promise | void: "Load older" |
Mount <Toaster /> once in your layout to show the messages when saving fails.
Keyboard
| Key | Does |
|---|---|
| ↑ ↓ | Move between notifications |
| Home End | First and last |
| Enter | Open it |
| Tab | To its mark-as-read and mute buttons, then on |
| Escape | Close the inbox and return to the bell |
Accessibility
- The bell is named with the unread count: “Notifications, 3 unread”.
- Each day is a labeled list; unread ones say so after the time.
- New notifications are announced politely, without moving focus.
- Only one notification is in the Tab order at a time; arrows move between them.
- Quick actions show on hover or focus, and always on touch screens.
- With reduced motion, rows and the count appear without growing.