<ui-notifications>ui-notifications.mjs · ui-notification-item.mjs
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.
<button> with
aria-haspopup="dialog", aria-expanded, and an
aria-label that includes the current item count (e.g.
"Warnings, 2") — JAWS / NVDA announce the count when the user
tabs onto the bell icon.aria-hidden so it's not read
twice.role="dialog" with aria-label from
the host's label attribute — or, when no label was
set by the time it was built, aria-labelledby pointing at the
trigger, so its name follows the trigger's from then on.label and updates the trigger's accessible name when it changes. This supports data-i18n-attr="label:some.key" being translated after the catalog loads and the trigger already exists. See Conventions.
role="list" so
list-style: none doesn't strip semantics in Safari /
VoiceOver.<ui-notification-item> gets
role="listitem" on connection, and decorative icons
(icon, action-icon) are marked
aria-hidden so only the message and action label are part of
the accessible name.<ui-popover>, this panel cancels Escape without stopping propagation; the sections below explain what ancestor handlers receive.
<ui-statusbar> displays the top active notification inline. Its role="status" region announces new or changed messages politely, without requiring the user to open the notification panel.
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.
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.
| Element | Attributes | Events |
|---|---|---|
<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.
|
open | true while the panel is showing. |
popover_id | The panel's id, for wiring another element's popovertarget at it. |
data-severity | Written by the host: none, info, warning, or error — the highest severity in the list. |
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.
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.
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.
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.
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.
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.
<ui-notification-item> children.prefers-reduced-motion: reduce the bell neither swings nor sends out a ring, although .attention-pulse elsewhere keeps a ring that fades in place.<ui-icon> child gets the ring without the swing.<ui-notifications> projects the whole store, so a store item added here would appear in each demo above.<ui-statusbar>'s role="status" region, or the page's own announcement, reports the new item.