<ui-notifications>

ui-notifications.mjs · ui-notification-item.mjs

Overview

A header-mounted list of persistent warning items. Each item is a single row with an icon, a message, and an action button — the action never wraps to a new row when the message is long (the message wraps instead). When the list is empty the bell stays visible but its trigger is disabled — there's nothing to show, so the popover can't be opened.

Application notifications come from the notifications store, shared with <ui-statusbar>. The component listens for the store's change event and updates its data-notif-id rows at the top of the list. Declarative <ui-notification-item> children, as used in these demos, have no such id and remain after the store-managed rows.

Accessibility (WCAG)

Use toast() for transient announcements without a matching statusbar entry, such as a one-time success message. <ui-statusbar> keeps the highest-priority current message visible. The bell panel lists all notifications on demand, including resolved entries that the user may still want to read.

Handling Escape during event dispatch

When focus is inside the panel, its keydown listener cancels Escape, closes the panel, and returns focus to the trigger. It does not stop propagation. Ancestor listeners receive the event after the panel has closed, so they must check e.defaultPrevented; an open-overlay query alone cannot show that the panel already handled the event.

Cancelling Escape also prevents the browser from closing a second overlay. The panel uses popover="auto", which registers a browser close watcher. The browser processes Escape after dispatch, when this handler has already closed the panel. Without cancellation, the browser could close the next overlay, such as the containing <dialog>.

When focus is outside the panel, its key listener does not receive Escape. The browser closes the panel after dispatch instead. An ancestor handler then sees defaultPrevented as false while the panel still matches an open-overlay query. Check both conditions: focus determines whether the component or browser handles the closing.

Compare the menu, which also cancels without stopping propagation; the popover, which additionally stops events it handles; and the combobox, which cancels only when it has an action to perform.

A select can also leave Escape uncancelled while its picker is open. If the picker has no focusable option, focus stays on the trigger, whose listener does not handle Escape. The browser closes the picker after dispatch. This is another reason to check both defaultPrevented and whether an overlay remains open.

API

ElementAttributesEvents
<ui-notifications> label, placement ui-notifications-toggle (bubbles, detail.open)
<ui-notification-item> type (info/warning/error), icon, action-label, action-href, action-icon, pre-rendered notification-action (bubbles, from non-link actions)

An item builds its own inner DOM on connection from those attributes, each of which is also a property (type, icon, action_label, action_href, action_icon). pre-rendered is the opt-out: with it present the element keeps the inner DOM it was given, which is how the notifications store owns the markup of the rows it manages.

On <ui-notifications>Description
show() / hide() Open or close the panel programmatically. Use this for remote controls such as the <ui-statusbar> expand button when popovertarget cannot be used.
opentrue while the panel is showing.
popover_idThe panel's id, for wiring another element's popovertarget at it.
data-severityWritten by the host: none, info, warning, or error — the highest severity in the list.

1. Empty state — greyish bell

With no <ui-notification-item> children the host stays visible but the bell is coloured var(--text-2) (data-severity="none") — the muted text tone, so an idle bell sits at the same weight as the neutral controls a header puts beside it. The trigger is also disabled in this state, so the empty popover can't be opened; it re-enables as soon as an item is added.

Show code

2. Severity — info / warning / error

Each <ui-notification-item> declares a type: info (blue, default), warning (orange), or error (red). The <ui-notifications> host picks the highest severity present (error > warning > info) and exposes it as data-severity, which colours the bell and the count badge via the internal --_severity-color custom property.

Show code

3. Multiple items — count badge and nowrap action

With more than one item the trigger shows a count badge (aria-hidden; the number is already announced as part of the button's accessible name). The badge takes the colour of the highest severity present. The rightmost item has a very long message to demonstrate that the action button stays on the first row while the message wraps onto multiple lines.

Show code

4. Placement

The placement attribute supports four positions. bottom-end, the default, opens down and left from the trigger's bottom-right corner and suits a bell in the header's top-right corner. Use bottom-start for a trigger on the left, or top-* near the viewport bottom, such as beside a footer <ui-statusbar>. The panel does not flip automatically. It stays on the selected side and scrolls within the available height, so a bottom placement near the viewport bottom produces a short panel.

Show code

5. Imperative API — appending items at runtime

New <ui-notification-item> children appended to the host are automatically relocated into the popover list by a MutationObserver. Existing items can be removed with .remove() — the host stays where it is and its trigger goes back to disabled once the list is empty, closing the panel first if the last item went while it was open.

Show code

6. A new item rings the bell

A trigger at the edge of a page is easy to miss, so an item that arrives in the store rings the bell. The trigger's icon lifts 2px and swings from its top centre in shrinking arcs over 0.9 s. As the first swing peaks, a .attention-pulse ring goes out from the trigger in the bell's severity colour.