v2.6.0

<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

Harbour & Pine - Operations See all activity

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 &amp; 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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

SlotDescription
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

PartDescription
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

PropertyDescription
--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).