Notification Center
Accessible, unstyled notification feed and inbox that can record toast history.
|
|
| Package |
@vizejs/ui/notification-center |
| Maturity |
stable |
| Own the source |
vize lib pull notification-center |
| Requires |
context, controllable-state, id |
| Aliases |
notification center, inbox, activity feed, toast history, notifications |
| Covers |
WAI-ARIA Feed pattern, Mantine Notifications, Novu Inbox, Sonner toast history recipes |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { NotificationCenter, NotificationCenterRoot, NotificationCenterEmpty, NotificationCenterItem, NotificationCenterList, NotificationCenterTrigger } from "@vizejs/ui/notification-center";
Or copy the source into your project with vize lib pull notification-center (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Bell trigger that discloses a notification feed where each entry can be marked read. -->
<script setup lang="ts">
import {
NotificationCenter,
NotificationCenterEmpty,
NotificationCenterItem,
NotificationCenterList,
NotificationCenterTrigger,
} from "@vizejs/ui/notification-center";
import type { NotificationInput } from "@vizejs/ui/notification-center";
const initial: readonly NotificationInput[] = [
{ id: "build", title: "Build passed", description: "main is green again.", createdAt: 2 },
{
id: "review",
title: "Review requested",
description: "Ada asked you to review #42.",
createdAt: 1,
},
];
// A fixed clock keeps server and client markup identical.
const now = (): number => 3;
</script>
<template>
<NotificationCenter :initial :now label="Notifications">
<NotificationCenterTrigger>Notifications</NotificationCenterTrigger>
<NotificationCenterList>
<template #item="{ notification, position, setSize }">
<NotificationCenterItem
v-slot="{ titleId, descriptionId, markRead }"
:notification
:position
:set-size
>
<strong :id="titleId">{{ notification.title }}</strong>
<p :id="descriptionId">{{ notification.description }}</p>
<button v-if="!notification.read" type="button" @click="markRead">Mark as read</button>
</NotificationCenterItem>
</template>
<NotificationCenterEmpty>You're all caught up.</NotificationCenterEmpty>
</NotificationCenterList>
</NotificationCenter>
</template>
API
NotificationCenter<Data = unknown>
Source: src/families/feedback/notification-center/notification-center-root.vue
Props
| Prop |
Type |
Default |
Description |
store |
NotificationStore<Data> |
undefined |
External store, for example one shared with a router guard or fed by connectNotificationSource. undefined creates a store per root. |
initial |
readonly NotificationInput<Data>[] |
undefined |
Notifications seeded into the root-owned store. |
maxLength |
number |
100 |
History limit of the root-owned store. |
now |
() => number |
Date.now |
Clock of the root-owned store; inject one for deterministic SSR. |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
label |
string |
"Notifications" |
Accessible name of the feed and base label of the trigger. |
inline |
boolean |
false |
Render the feed permanently (inbox page) instead of as a disclosure. |
open |
boolean |
undefined |
Controlled disclosure state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial disclosure state for uncontrolled use. |
Events
| Event |
Payload |
Description |
update:open |
[value: boolean] |
Fired when the disclosure requests a controlled open value. |
open-change |
[value: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request. |
Slots
| Slot |
Slot props |
Description |
default |
NotificationCenterSlotState |
Trigger, list, and empty-state children. |
Exposed
| Member |
Type |
Description |
count |
— |
|
listId |
— |
|
open |
— |
|
setOpen |
— |
|
state |
— |
|
store |
— |
|
unreadCount |
— |
|
NotificationCenterRoot<Data = unknown>
Source: src/families/feedback/notification-center/notification-center-root.vue
Props
| Prop |
Type |
Default |
Description |
store |
NotificationStore<Data> |
undefined |
External store, for example one shared with a router guard or fed by connectNotificationSource. undefined creates a store per root. |
initial |
readonly NotificationInput<Data>[] |
undefined |
Notifications seeded into the root-owned store. |
maxLength |
number |
100 |
History limit of the root-owned store. |
now |
() => number |
Date.now |
Clock of the root-owned store; inject one for deterministic SSR. |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
label |
string |
"Notifications" |
Accessible name of the feed and base label of the trigger. |
inline |
boolean |
false |
Render the feed permanently (inbox page) instead of as a disclosure. |
open |
boolean |
undefined |
Controlled disclosure state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial disclosure state for uncontrolled use. |
Events
| Event |
Payload |
Description |
update:open |
[value: boolean] |
Fired when the disclosure requests a controlled open value. |
open-change |
[value: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request. |
Slots
| Slot |
Slot props |
Description |
default |
NotificationCenterSlotState |
Trigger, list, and empty-state children. |
Exposed
| Member |
Type |
Description |
open |
boolean |
Whether the list is shown (always true for inline centers). |
state |
NotificationCenterState |
Stable state token. |
unreadCount |
number |
Unread active notifications. |
count |
number |
Active notifications. |
store |
NotificationStore<Data> |
The store driving this center. |
listId |
string |
Id of the feed element. |
setOpen |
(value: boolean, event?: Event | null) => boolean |
Request an open value. Returns whether it changed. |
NotificationCenterEmpty
Source: src/families/feedback/notification-center/notification-center-empty.vue
Slots
| Slot |
Slot props |
Description |
default |
— |
Empty-state contents, shown only while no active notification exists. |
NotificationCenterItem<Data = unknown>
Source: src/families/feedback/notification-center/notification-center-item.vue
Props
| Prop |
Type |
Default |
Description |
notification (required) |
NotificationRecord<Data> |
required |
Notification snapshot to render. |
position (required) |
number |
required |
One-based position in the feed (aria-posinset). |
setSize (required) |
number |
required |
Feed size (aria-setsize); -1 when unknown. |
markReadOnActivate |
boolean |
true |
Mark the notification read when the article itself is clicked or receives Enter. |
Events
| Event |
Payload |
Description |
activate |
[notification: NotificationRecord<Data>, nativeEvent: Event] |
Fired when the article is activated by click or Enter. |
Slots
| Slot |
Slot props |
Description |
default |
NotificationCenterItemSlotState<Data> |
Article contents. Put titleId/descriptionId on the visible headline and text. |
Exposed
| Member |
Type |
Description |
element |
HTMLElement | null |
Rendered article. |
NotificationCenterList
Source: src/families/feedback/notification-center/notification-center-list.vue
Props
| Prop |
Type |
Default |
Description |
busy |
boolean |
false |
Whether the feed is loading or inserting articles (aria-busy). |
markReadOnActivate |
boolean |
true |
Default for rendered items: mark a notification read when its article is activated. |
closeOnEscape |
boolean |
true |
Let Escape inside a disclosure-style feed close it and refocus the trigger. |
Slots
| Slot |
Slot props |
Description |
item |
NotificationCenterListItemSlotProps |
Render one notification, normally with NotificationCenterItem. |
default |
— |
Content after the articles, for example an empty state or a load-more button. |
Exposed
| Member |
Type |
Description |
element |
HTMLElement | null |
Rendered feed element. |
focusItem |
(index: number) => boolean |
Focus the article at a zero-based index. Returns whether it moved focus. |
NotificationCenterTrigger
Source: src/families/feedback/notification-center/notification-center-trigger.vue
Props
| Prop |
Type |
Default |
Description |
disabled |
boolean |
false |
Remove the trigger from activation and sequential keyboard focus. |
formatLabel |
(label: string, unreadCount: number) => string |
${label}, ${count} unread when unread notifications exist, else label |
Build the accessible name from the root label and unread count. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before the trigger toggles the center. Call preventDefault() to keep state unchanged. |
Slots
| Slot |
Slot props |
Description |
default |
NotificationCenterSlotState |
Trigger contents, typically an icon and an unread badge. |
Exposed
| Member |
Type |
Description |
element |
HTMLButtonElement | null |
Rendered native button. |
focus |
(options?: FocusOptions) => void |
Move focus to the trigger. |
Behavior
Normative behavior for @vizejs/ui/notification-center: an SSR-safe notification history
store and an APG feed inbox. It can serve
as toast history by feeding it through connectNotificationSource. Every row is proven by
the named test.
| State x input |
Observable outcome |
Proven by |
store add |
Notifications are frozen snapshots, newest first, with "{idPrefix}-{n}" ids and timestamps from the injected now. |
adds notifications newest first with generated ids and injected timestamps |
store add with an existing id |
The notification is replaced in place. |
re-adding an id replaces it in place |
markRead/markUnread/update/archive/remove |
Each reports whether anything changed; archived notifications leave the feed and the unread count. |
read, archive, update, and remove report whether anything changed |
markAllRead |
Only active unread notifications change; the count is returned. |
markAllRead only touches active unread notifications |
groups |
Active notifications grouped by key in first-appearance order with unread counts. |
groups preserve first appearance order with per-group unread counts |
maxLength exceeded |
The oldest notifications are evicted. |
maxLength evicts the oldest notifications |
initial, clear |
Seeds are sorted by createdAt; clear empties the store. |
initial notifications and clear |
connectNotificationSource |
Source emissions are added until the returned function unsubscribes. |
connectNotificationSource feeds the store until unsubscribed |
| invalid option or type |
Throws VIZE_UI_NOTIFICATION_OPTION. |
invalid options and inputs throw diagnostics |
| disclosure trigger click |
The trigger toggles aria-expanded, controls the feed id, names the unread count, and the feed stops being hidden. |
trigger discloses the feed and announces the unread count |
| rendered feed |
role="feed" with aria-label/aria-busy; articles are focusable with aria-posinset, aria-setsize, title/description wiring, data-read, data-type, data-group; inline centers omit aria-expanded. |
feed renders APG articles with position, size, labels, and read state |
| PageDown / PageUp inside an article |
Focus moves to the next or previous article; at the edges focus stays and the key is not consumed. |
PageDown and PageUp move focus between articles |
| click or Enter on an article |
The notification is marked read and activate fires unless markReadOnActivate is false; clicks on inner controls are ignored. |
activating an article marks it read unless opted out |
| Escape inside a disclosure feed |
The feed closes and focus returns to the trigger. |
Escape closes a disclosure feed and returns focus to the trigger |
custom item slot |
Consumers render NotificationCenterItem with typed data and receive ids plus markRead/archive/remove actions. |
custom item slots receive typed actions and ids |
| root-owned store |
The root creates, exposes, and injects its store; useNotificationCenter() reads it; the empty state shows when nothing remains. |
root owns a store, exposes it, and useNotificationCenter reads it |
| parts or composable outside the root |
Throws VIZE_UI_CONTEXT_MISSING. |
parts and the composable require a NotificationCenterRoot |
| SSR |
Isolated requests render byte-identical feeds with deterministic ids. |
renders byte-identical feed markup across isolated SSR requests |
| hydration |
The feed hydrates without warnings or node replacement. |
hydrates the feed without diagnostics |
| public types |
Data flows through records, slots, expose, and useNotificationCenter<Data>(); types are closed unions. |
src/families/feedback/notification-center/notification-center.types.test-d.ts |
Components
| Component |
State x input |
Outcome |
notification-center-root.vue |
store, inline, open |
Creates or adopts the store, owns disclosure state, provides context and the store. |
notification-center-trigger.vue |
click |
Toggles the feed and labels itself with the unread count. |
notification-center-list.vue |
notifications, PageUp/PageDown, Esc |
Renders the role="feed" element and one article per active notification. |
notification-center-item.vue |
click, Enter |
Renders one article with feed positions and marks it read on activation. |
notification-center-empty.vue |
no active notifications |
Shows the empty-state text; hidden otherwise. |
Parts And Data
| Target |
Public contract |
| Root |
data-vize-ui="notification-center-root", part="root", data-state, data-inline, data-unread-count |
| Trigger |
data-vize-ui="notification-center-trigger", part="trigger", data-unread-count, data-has-unread |
| List |
data-vize-ui="notification-center-list", part="list", role="feed", data-count |
| Item |
data-vize-ui="notification-center-item", part="item", data-read, data-type, data-group |
| Empty |
data-vize-ui="notification-center-empty", part="empty" |
The feed pattern's Control+Home / Control+End exits are left to the surrounding page.
NotificationCenter ships no stylesheet.