Skip to content
Communication

Notification Center

The bell in the top bar, and everything behind it.

Walkthrough

1. Basic

The component never changes the list itself: onread, onreadall and ondismiss are requests, and the list you hand back is the answer. Hover a row for its ×; on a phone, swipe it.

Acme

<script lang="ts">	import { NotificationCenter, Text, type NotificationItem } from 'omaris';​	const now = Date.now();	const minutes = (m: number) => now - m * 60_000;​	let notifications = $state<NotificationItem[]>([		{			id: '1',			title: 'Amina Yusuf mentioned you in Checkout redesign',			body: 'Can you look at the empty cart state before Friday?',			time: minutes(2),			avatar: { name: 'Amina Yusuf' }		},		{			id: '2',			title: 'Deploy to production finished',			body: 'storefront@4f2c1a is live in every region.',			time: minutes(38),			icon: rocket		},		{			id: '3',			title: 'Dilan Karim assigned you Fix invoice rounding',			time: minutes(60 * 5),			avatar: { name: 'Dilan Karim' }		},		{			id: '4',			title: 'Hevi Salih approved Release 2.4',			time: minutes(60 * 26),			read: true,			avatar: { name: 'Hevi Salih' }		},		{			id: '5',			title: 'Weekly report is ready',			body: 'Orders up 12% on last week.',			time: minutes(60 * 24 * 9),			read: true,			icon: chart		}	]);</script>​{#snippet rocket()}	...{/snippet}{#snippet chart()}	...{/snippet}​<div	class="flex w-full items-center justify-between rounded-shape-md border border-border bg-surface-container-low px-4 py-2">	<Text variant="title-md">Acme</Text>	<NotificationCenter		{notifications}		onread={(id) =>			(notifications = notifications.map((n) => (n.id === id ? { ...n, read: true } : n)))}		onreadall={() => (notifications = notifications.map((n) => ({ ...n, read: true })))}		ondismiss={(id) => (notifications = notifications.filter((n) => n.id !== id))}	/></div>

2. Long backlog

A thousand of them. Past virtualize rows (50 by default) the list is a VirtualList, so the panel opens as fast with a backlog as without, and the Unread tab counts on the bell and in the tab alike.

Orders

<script lang="ts">	import { NotificationCenter, Text, type NotificationItem } from 'omaris';​	const PEOPLE = ['Amina Yusuf', 'Dilan Karim', 'Hevi Salih', 'Omar Nasir', 'Sara Kamal'];	const WHAT = ['commented on', 'reacted to', 'shared', 'edited', 'moved'];	const now = Date.now();​	let notifications = $state<NotificationItem[]>(		Array.from({ length: 1000 }, (_, i) => ({			id: String(i),			title: `${PEOPLE[i % 5]} ${WHAT[(i * 3) % 5]} Order ${4200 - i}`,			time: now - i * 47 * 60_000,			read: i > 12 && i % 7 !== 0,			avatar: { name: PEOPLE[i % 5] }		}))	);</script>​<div	class="flex w-full items-center justify-between rounded-shape-md border border-border bg-surface-container-low px-4 py-2">	<Text variant="title-md">Orders</Text>	<NotificationCenter		{notifications}		onread={(id) =>			(notifications = notifications.map((n) => (n.id === id ? { ...n, read: true } : n)))}		onreadall={() => (notifications = notifications.map((n) => ({ ...n, read: true })))}		ondismiss={(id) => (notifications = notifications.filter((n) => n.id !== id))}	/></div>

3. Full screen

Below mobileQuery the panel is a screen of its own: a back arrow, the title and "Mark all read" in a top app bar, and the list below. It is forced here with a query that always matches, so it shows on a desktop too: swipe a row left to dismiss it, right to mark it read.

<script lang="ts">	import { NotificationCenter, type NotificationItem } from 'omaris';​	const now = Date.now();​	let notifications = $state<NotificationItem[]>([		{			id: 'a',			title: 'Your order from Zagros Café is on its way',			body: 'Arriving in about 20 minutes.',			time: now - 4 * 60_000,			avatar: { name: 'Zagros Café' }		},		{			id: 'b',			title: 'Sara Kamal sent you 25,000 IQD',			time: now - 3 * 3600_000,			avatar: { name: 'Sara Kamal' }		},		{			id: 'c',			title: 'Rate your last ride',			body: 'Tell us how it went with Omar.',			time: now - 30 * 3600_000,			read: true,			avatar: { name: 'Omar Nasir' }		}	]);</script>​<NotificationCenter	{notifications}	mobileQuery="(min-width: 0px)"	onread={(id) =>		(notifications = notifications.map((n) => (n.id === id ? { ...n, read: true } : n)))}	onreadall={() => (notifications = notifications.map((n) => ({ ...n, read: true })))}	ondismiss={(id) => (notifications = notifications.filter((n) => n.id !== id))}/>

4. Overridden

classes reaches the bell, the badge, the panel and every part of a row; icon replaces the bell, footer adds a way out, empty replaces both empty states, and every label can be translated.

CI

<script lang="ts">	import { Button, NotificationCenter, Text, type NotificationItem } from 'omaris';​	const now = Date.now();​	let notifications = $state<NotificationItem[]>([		{ id: 'x', title: 'Build 812 failed on main', time: now - 90_000 },		{ id: 'y', title: 'Build 811 passed', time: now - 40 * 60_000, read: true }	]);</script>​<div	class="flex w-full items-center justify-between rounded-shape-md border border-border bg-surface-container-low px-4 py-2">	<Text variant="title-md">CI</Text>	<NotificationCenter		{notifications}		title="Builds"		label="Build alerts"		markAllLabel="Acknowledge all"		unreadLabel="New"		tab="unread"		onread={(id) =>			(notifications = notifications.map((n) => (n.id === id ? { ...n, read: true } : n)))}		onreadall={() => (notifications = notifications.map((n) => ({ ...n, read: true })))}		classes={{			bell: 'text-destructive',			panel: 'w-80',			row: 'data-unread:bg-destructive-container/40',			dot: 'bg-destructive',			title: 'font-mono'		}}	>		{#snippet icon()}			...		{/snippet}		{#snippet empty(tab)}			<Text variant="body-md" tone="muted" align="center">				{tab === 'unread' ? 'Every build acknowledged.' : 'No builds yet.'}			</Text>		{/snippet}		{#snippet footer({ close })}			<Button variant="text" size="xs" onclick={close}>Close</Button>		{/snippet}	</NotificationCenter></div>