Communication
Notification Center
The bell in the top bar, and everything behind it.
import { NotificationCenter } from 'omaris' Examples
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> 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> 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))}/> 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> When to use it
Use it for
- The bell in an app's top bar: a count of what is unread, and a panel of the latest activity behind it — mentions, assignments, approvals, finished jobs.
- An inbox people triage in place: open one to mark it read, "Mark all read" for the rest, swipe or × to dismiss.
- A long backlog. Past
virtualizerows the list is windowed, so a thousand notifications open as fast as ten. - The same feature on a phone and a desktop: a popover under the bell above
mobileQuery, a full-screen view with a back arrow below it.
Not for
- Telling someone that what they just did finished → Toast. The center is the history; a toast is the moment.
- A problem with the page they are on → Alert, inline where it applies.
- A full-page inbox with filters, search and bulk selection → List or Table on a route of its own; link to it from
footer. - A count on a navigation destination → Badge on that destination.
Do
- Own the list.
onread,onreadallandondismissare requests: updatenotificationsin them (and on the server), and the panel follows. - Give system notifications an
iconand people's anavatar, so the two read apart at a glance. - Write the
titleas the whole sentence — "Amina mentioned you in Checkout" — and keepbodyfor the quote or the detail. - Pass
hreffor anything with a page behind it; opening it marks it read and closes the panel. - Set
locale(or<html lang>) so the times are in the app's language —ar,ckbandtrship with it, and the digits stay Western in all of them.
Don't
- Toast a notification and also put it in the center. Pick one: the moment, or the history.
- Leave out
ondismissand then expect a swipe to do anything — without it rows cannot be dismissed, by design. - Put actions inside a row (Approve, Reply). A row is one press; the action belongs on the page
hrefopens. - Mark everything read when the panel opens. People open it to see what is new; reading is their call.
API
NotificationCenter
The bell in the top bar, and everything behind it.
A bell with the unread count on it opens a panel: a title and "Mark all read", All and Unread tabs, and the notifications themselves — an unread dot, an avatar or an icon, the title and a line or two of body, and a time that keeps itself current ("3m", then "4m"). On a desktop the panel hangs off the bell; on a phone it is a screen of its own — a top app bar with a back arrow, sliding in from the edge the way a page does — because a popover a thumb has to reach across for is the wrong shape there, and an inbox wants the whole height and nothing else pulling on the rows.
The bell rings once when the unread count goes up, and not at all under reduced motion.
Rows are dismissed the way each device expects. A finger swipes one away — or swipes the other way to mark it read — and a mouse gets an × that appears where the time was. Either way the row folds shut before ondismiss runs, so the list closes over the gap rather than jumping.
The component never changes notifications itself: every callback is a request, and the list you pass back is the answer. Past virtualize rows the list is windowed, so a backlog of a thousand opens as fast as one of ten.
import { NotificationCenter } from 'omaris' <NotificationCenter {notifications} onread={(id) => markRead(id)} onreadall={markAllRead} ondismiss={(id) => remove(id)}/> Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- The box around the bell and its count —
relative, so the badge can pin to it. bell- The bell — an
IconButton, so this lands on its root. It swings from the top. badge- The unread count on the bell — a
Badge. panel- The popover's surface, on a desktop.
screen- The full-screen view on a phone — a native modal
<dialog>. header- Title and "Mark all read" — the top app bar on a phone.
back- The back arrow in the phone's top app bar — an
IconButton. heading- No description in the source yet.
markAll- "Mark all read" — a
Button. tabs- The tab set.
tabPanel- One tab's panel — the box the list scrolls inside.
list- The scrolling list.
item- One notification's wrapper — it folds shut when the row is dismissed.
row- The pressable row — a link with
href, a button without. dot- The unread dot, at the start of an unread row.
media- The avatar, or the icon well.
icon- The icon well, when a row has an
iconrather than anavatar. text- No description in the source yet.
title- No description in the source yet.
body- No description in the source yet.
time- The relative time. Gives way to the × under a hovering mouse.
dismiss- The ×. A mouse gets it on hover; a finger swipes instead.
empty- No description in the source yet.
footer- No description in the source yet.
Props
notifications required NotificationItem[] open bindableDefaults to false
boolean Whether the panel is open. Bindable.
tab bindableDefaults to 'all'
NotificationTab The tab showing. Bindable.
title Defaults to 'Notifications'
string The panel's heading, and its accessible name.
label Defaults to 'Notifications'
string The bell's accessible name. The unread count is added to it.
onread (id: string) => void A notification was opened, or swiped the other way: mark it read.
onreadall () => void "Mark all read" was pressed. Without it the button is not drawn.
ondismiss (id: string) => void A notification was dismissed — swiped away, or its × pressed. Called once the row has folded shut. Without it rows cannot be dismissed.
onselect (notification: NotificationItem) => void A notification was pressed, after onread.
empty Snippet<[NotificationTab]> Drawn when the tab has nothing in it. Handed the tab.
footer Snippet<[{ close: () => void }]> Along the bottom of the panel — a "See all" link, say.
icon Snippet Replaces the bell glyph.
locale string Language for the relative times. Defaults to <html lang>.
max Defaults to 99
number The count stops at this and adds a +.
virtualize Defaults to 50
number Past this many rows the list is windowed.
align Defaults to 'end'
AnchorAlign Which edge of the bell the popover lines up with.
mobileQuery Defaults to '(max-width: 639px)'
string What counts as a phone — there the panel is a full-screen view.
markAllLabel Defaults to 'Mark all read'
string Labels, for another language.
allLabel Defaults to 'All'
string unreadLabel Defaults to 'Unread'
string dismissLabel Defaults to 'Dismiss'
string readLabel Defaults to 'Read'
string closeLabel Defaults to 'Back'
string The back arrow that closes the full-screen view on a phone.
class string classes NotificationCenterClasses Per-part Tailwind overrides. class still covers the root.