<bmx-notification-center>
The inbox every application ends up needing: a bell with the unread count, and a panel listing what happened - grouped into Today, Yesterday and earlier, with similar ones stacked ("+3 more like this"), each one read or unread, pinnable, snoozable and dismissable, with its own action buttons.
16 properties · 5 events · 12 methods · 18 parts
Example
Open the bell. Every change arrives in bmxChange with the whole list, for the page to keep.
Show markup
<div class="ex-nc-bar" style="display: flex; align-items: center; gap: 12px; padding: 8px 12px; border: 1px solid var(--bmx-border); border-radius: 12px; background: var(--bmx-surface-raised)">
<strong style="flex: 1 1 auto">Harbour & Pine - Operations</strong>
<button type="button" id="ex-nc-send" style="font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid var(--bmx-border-strong); background: var(--bmx-surface); color: var(--bmx-text); cursor: pointer">Send a test notification</button>
<bmx-notification-center id="ex-nc" searchable>
<a slot="footer" href="#ex-nc" style="color: var(--bmx-primary-soft-text); font-size: 0.8125rem">See all activity</a>
</bmx-notification-center>
</div>
<p class="note" id="ex-nc-out" role="status" style="margin: 12px 0 0">Open the bell. Every change arrives in <code>bmxChange</code> with the whole list, for the page to keep.</p>
<script type="module">
await customElements.whenDefined('bmx-notification-center');
const inbox = document.getElementById('ex-nc');
const out = document.getElementById('ex-nc-out');
const now = Date.now();
const min = 60_000;
const hour = 60 * min;
const day = 24 * hour;
inbox.sources = [
{ id: 'approvals', label: 'Approvals', description: 'Invoices and purchase orders waiting on you' },
{ id: 'builds', label: 'Builds', description: 'Pipelines on the main branch' },
{ id: 'mentions', label: 'Mentions', description: 'When someone mentions you' },
{ id: 'digest', label: 'Digest', description: 'The weekly summary' },
];
inbox.notifications = [
{ id: 'po-311', title: 'Purchase order PO-311 needs your approval', body: 'Office furniture for the Leeds site, £4,820.00, raised by Tom Okafor.', time: now - 4 * min, source: 'approvals', level: 'warning', initials: 'TO', actions: [{ id: 'approve', label: 'Approve', primary: true }, { id: 'review', label: 'Review' }] },
{ id: 'inv-204', title: 'Invoice INV-204 approved', body: 'Approved by Priya Shah.', time: now - 18 * min, source: 'approvals', level: 'success', group: 'approved', initials: 'PS' },
{ id: 'inv-205', title: 'Invoice INV-205 approved', body: 'Approved by Priya Shah.', time: now - 26 * min, source: 'approvals', level: 'success', group: 'approved', initials: 'PS' },
{ id: 'inv-209', title: 'Invoice INV-209 approved', body: 'Approved by Priya Shah.', time: now - 41 * min, source: 'approvals', level: 'success', group: 'approved', initials: 'PS', read: true },
{ id: 'build-1142', title: 'Build 1142 failed on main', body: 'Two tests failed in the payments service.', time: now - 2 * hour, source: 'builds', level: 'danger', actions: [{ id: 'logs', label: 'View logs' }] },
{ id: 'mention-1', title: 'Sam Rivera mentioned you in “Q4 forecast”', body: '“Can you check the freight figures before Friday?”', time: now - 5 * hour, source: 'mentions', initials: 'SR' },
{ id: 'build-1139', title: 'Build 1139 passed', time: now - day - 3 * hour, source: 'builds', level: 'success', read: true },
{ id: 'mention-0', title: 'Amira Haddad replied to your comment', body: '“Agreed - let’s move the stock count to Monday.”', time: now - day - 6 * hour, source: 'mentions', initials: 'AH', read: true },
{ id: 'digest-41', title: 'Your weekly digest is ready', body: '38 invoices processed, 3 overdue, 1 dispute open.', time: now - 3 * day, source: 'digest', level: 'info', pinned: true, read: true },
{ id: 'digest-40', title: 'Your weekly digest is ready', time: now - 10 * day, source: 'digest', level: 'info', read: true },
];
inbox.addEventListener('bmxChange', e => {
out.textContent = `bmxChange: ${e.detail.reason} (${e.detail.ids.join(', ')}). ${e.detail.notifications.length} notifications kept.`;
});
inbox.addEventListener('bmxAction', e => {
out.textContent = `bmxAction: “${e.detail.action.label}” on ${e.detail.id}.`;
});
// A reused pool of sample messages: the button adds one of them each time.
const samples = [
{ title: 'Invoice INV-212 is overdue', body: 'Fernbrook Supplies, £1,240.00, due 3 days ago.', source: 'approvals', level: 'warning' },
{ title: 'Build 1143 passed on main', source: 'builds', level: 'success' },
{ title: 'Leo Martins mentioned you in “Supplier review”', body: '“Adding you so you see the new terms.”', source: 'mentions', initials: 'LM' },
];
let sent = 0;
document.getElementById('ex-nc-send').addEventListener('click', () => {
const sample = samples[sent % samples.length];
sent += 1;
inbox.add({ ...sample, id: `sample-${sent}`, time: Date.now() });
});
</script>
<bmx-notification-center id="inbox"></bmx-notification-center>
<script>
inbox.sources = [{ id: 'approvals', label: 'Approvals' }, { id: 'builds', label: 'Builds' }];
inbox.notifications = await api.notifications();
inbox.addEventListener('bmxChange', e => api.save(e.detail.notifications));
socket.onmessage = m => inbox.add(JSON.parse(m.data));
</script>
The list is the page's: every change the reader makes arrives in
bmxChange with the whole new list and the reason, and nothing is stored
in the browser. add() puts new ones in as they arrive, and they are
announced to screen readers ("New notification: Invoice approved"), politely,
or at once for a danger one.
Tabs show everything, the unread ones, and the snoozed ones (with "Show
now"). With more than one source there is a source filter, and the settings
page lets the reader mute a source - muted ones are hidden and not counted -
and turn stacking off; bmxPreferencesChange hands the choices back.
mode="inline" draws the panel in place (an activity page, a sidebar)
instead of under a bell.
The list is an ARIA feed of articles: arrow keys and Page Up/Down move between notifications, Home and End go to the ends, Enter opens one, Delete dismisses it, and Tab reaches the buttons of the one you are on. Each article's name says whether it is unread or pinned.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
headingLevel |
heading-level |
number |
2 |
The level of the panel's heading (2 to 6); the day headings are one below. |
hideSettings |
hide-settings |
boolean |
false |
Leave out the settings page. |
label |
label |
string |
— | The panel's heading, and the bell's name. Default: "Notifications". |
locale |
locale |
string |
— | The locale for times and dates. Default: the nearest lang attribute. |
max |
max |
number |
200 |
The most notifications kept; older ones beyond it are dropped (pinned ones never are). |
maxBadge |
max-badge |
number |
99 |
The bell shows "99+" past this. |
mode |
mode |
'popup' | 'inline' |
'popup' |
popup: a bell that opens the panel. inline: the panel drawn in place. |
notifications |
notifications |
BmxNotification[] | string |
[] |
The notifications, as objects or JSON. Mutable: the reader's changes are written back. |
now |
now |
number | string |
— | The time it is now, for the grouping and the "5 min ago" times (milliseconds or ISO). Default: the clock, followed every half minute. |
open |
open |
boolean |
false |
Whether the panel is open (popup mode). Mutable: the reader opens and closes it. |
placement |
placement |
BmxPlacement |
'bottom-end' |
Where the panel opens against the bell. |
searchable |
searchable |
boolean |
false |
Show a search box over the list. |
sources |
sources |
BmxNotificationSource[] | string |
[] |
The sources (channels), as objects, JSON or a comma list of ids. Mutable: muting is written back. |
stack |
stack |
boolean |
true |
Stack notifications that share a group on the same day. Mutable: the settings page changes it. |
strings |
strings |
Partial<BmxNotificationCenterStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
tab |
tab |
BmxNotificationTab |
'all' |
The tab showing. Mutable. |
Events
| Event | Detail | Description |
|---|---|---|
bmxAction |
BmxNotificationActionDetail |
Fired when an action button is pressed. Cancel it to leave the notification unread. |
bmxChange |
BmxNotificationChangeDetail |
Fired when the list changes, with the whole new list. |
bmxNotificationClick |
BmxNotificationClickDetail |
Fired when a notification is chosen. Cancel it to handle the choice yourself. |
bmxOpenChange |
boolean |
Fired when the panel opens or closes. |
bmxPreferencesChange |
BmxNotificationPreferencesDetail |
Fired when the reader mutes or unmutes a source or turns stacking on or off. |
Methods
| Method | Signature | Description |
|---|---|---|
add |
add(notifications: BmxNotification | BmxNotification[]) => Promise<string[]> |
Adds notifications (or replaces those with the same id) and announces the new unread ones. Resolves with their ids. |
clear |
clear() => Promise<void> |
Removes every notification. |
getNotifications |
getNotifications() => Promise<BmxNotification[]> |
The current list, as the element holds it. |
getUnreadCount |
getUnreadCount() => Promise<number> |
How many are unread (not snoozed, not muted). |
hide |
hide() => Promise<void> |
Closes the panel (popup mode). |
markAllRead |
markAllRead() => Promise<string[]> |
Marks every notification that is showing as read. Resolves with their ids. |
markRead |
markRead(ids: string | string[], read?: boolean) => Promise<void> |
Marks notifications read (or unread with read false). |
removeNotifications |
removeNotifications(ids: string | string[]) => Promise<void> |
Removes notifications. |
setFocus |
setFocus() => Promise<void> |
Focuses the bell (popup mode) or the list (inline mode). |
show |
show() => Promise<void> |
Opens the panel (popup mode). |
snooze |
snooze(id: string, until: BmxSnoozeOption | number | string | Date | null) => Promise<void> |
Snoozes one: until an instant (milliseconds, Date, ISO) or for hour, three-hours, tomorrow or next-week. Null wakes it. |
toggle |
toggle() => Promise<void> |
Opens the panel when closed, closes it when open. |
Slots
| Slot | Description |
|---|---|
empty |
Shown instead of "You are all caught up". |
footer |
Along the foot of the panel: "See all", a link to a settings page. |
CSS shadow parts
| Part | Description |
|---|---|
action |
An action button. |
avatar |
A notification's picture or initials. |
badge |
The unread count on the bell. |
bell |
The bell button (popup mode). |
body |
A notification's text. |
empty |
The empty message. |
feed |
The list of notifications. |
footer |
The foot of the panel. |
header |
The panel's heading row. |
item |
One notification; also item-unread. |
meta |
The source and time line. |
panel |
The panel. |
search |
The search box. |
section |
A day heading. |
sources |
The source filter. |
tabs |
The All / Unread / Snoozed tabs. |
title |
A notification's title. |
tools |
The read, pin, snooze and dismiss buttons. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-notification-center-avatar-size |
The size of a notification's picture. |
--bmx-notification-center-background |
The panel's background. |
--bmx-notification-center-badge-background |
The unread count's background. |
--bmx-notification-center-bell-size |
The bell button's size. |
--bmx-notification-center-height |
The tallest the panel grows (popup mode); the list scrolls inside it. |
--bmx-notification-center-radius |
Corner radius of the panel. |
--bmx-notification-center-shadow |
The floating panel's shadow. |
--bmx-notification-center-unread-background |
The background of an unread notification. |
--bmx-notification-center-width |
The panel's width (popup mode). |