The persistent inbox: a bell wearing the unread count and a floating panel of notifications with All and Unread filters, Mark all read, dismissible rows and a View all footer. Toast stays transient; this one remembers. Reach for it in the app header wherever activity should wait for the user.
<sui-notification-center align="start" width="360"></sui-notification-center>
<script type="module">
import { defineCustomElements } from "@shineui/core/loader";
import "@shineui/core/shineui.css";
defineCustomElements();
const el = document.querySelector("sui-notification-center");
el.notifications = [
{ id: "paid-1043", title: "Payment received, $6,420.00", detail: "Silver Hills Church paid INV-1043.", time: "11:02 AM", severity: "positive" },
{ id: "overdue", title: "2 invoices are overdue", detail: "Riverbend Dental Group and Kye's Catering.", time: "9:15 AM", severity: "critical" },
{ id: "close", title: "August close is ready for review", detail: "Two accounts still need a reconciliation.", time: "8:40 AM", severity: "attention" },
]; /* SuiNotificationItem[] */
el.addEventListener("suiItemPick", (e) => console.log(e.detail));
</script>
import { SuiNotificationCenter } from "@shineui/react";
import "@shineui/core/shineui.css";
export function Example() {
const notifications = [
{ id: "paid-1043", title: "Payment received, $6,420.00", detail: "Silver Hills Church paid INV-1043.", time: "11:02 AM", severity: "positive" },
{ id: "overdue", title: "2 invoices are overdue", detail: "Riverbend Dental Group and Kye's Catering.", time: "9:15 AM", severity: "critical" },
{ id: "close", title: "August close is ready for review", detail: "Two accounts still need a reconciliation.", time: "8:40 AM", severity: "attention" },
]; /* SuiNotificationItem[] */
return <SuiNotificationCenter align="start" width="360" notifications={notifications} onSuiItemPick={(e) => console.log(e.detail)} />;
}
<script setup>
import { SuiNotificationCenter } from "@shineui/vue";
import "@shineui/core/shineui.css";
const notifications = [
{ id: "paid-1043", title: "Payment received, $6,420.00", detail: "Silver Hills Church paid INV-1043.", time: "11:02 AM", severity: "positive" },
{ id: "overdue", title: "2 invoices are overdue", detail: "Riverbend Dental Group and Kye's Catering.", time: "9:15 AM", severity: "critical" },
{ id: "close", title: "August close is ready for review", detail: "Two accounts still need a reconciliation.", time: "8:40 AM", severity: "attention" },
]; // SuiNotificationItem[]
const onChange = (e) => console.log(e.detail);
</script>
<template>
<SuiNotificationCenter align="start" width="360" :notifications="notifications" @sui-item-pick="onChange" />
</template>
import { Component } from "@angular/core";
import { SuiNotificationCenter } from "@shineui/angular";
@Component({
standalone: true,
imports: [SuiNotificationCenter],
template: `<sui-notification-center align="start" width="360" [(open)]="open" [notifications]="notifications" (suiItemPick)="onChange($event)"></sui-notification-center>`,
})
export class Example {
open = false;
notifications = [
{ id: "paid-1043", title: "Payment received, $6,420.00", detail: "Silver Hills Church paid INV-1043.", time: "11:02 AM", severity: "positive" },
{ id: "overdue", title: "2 invoices are overdue", detail: "Riverbend Dental Group and Kye's Catering.", time: "9:15 AM", severity: "critical" },
{ id: "close", title: "August close is ready for review", detail: "Two accounts still need a reconciliation.", time: "8:40 AM", severity: "attention" },
]; // SuiNotificationItem[]
onChange(e: CustomEvent) { console.log(e.detail); }
}
Basic
The bell wears the unread count in an overlay badge and is named "Notifications, 3 unread", with aria-expanded and aria-haspopup="dialog". Each unread row also reads "Unread" to a screen reader.
Unread rows sit on the sunken surface with the orange dot. All and Unread filter the list, Mark all read clears the badge, and hover or keyboard focus reveals the dismiss control on a row.
<div style="padding:6px;min-height:460px">
<sui-notification-center id="nc-panel" open align="start" width="360"></sui-notification-center>
</div>
suiClose is cancelable. This one vetoes the outside-pointer close, so a stray click on the page leaves the panel up and only the bell or Escape closes it.
Open the bell, then click the page.
<div style="display:flex;gap:14px;align-items:flex-start;padding:6px;min-height:460px">
<sui-notification-center id="nc-veto" align="start" width="360"></sui-notification-center>
<span id="nc-veto-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Open the bell, then click the page.</span>
</div>
Items are { id, title, detail, time, severity, read }. Activating a row emits suiItemPick and marks it read, which emits suiRead once; hover reveals dismiss, which emits suiRemove. Listen to suiViewAll to route to your activity page.
Keyboard
Key
Action
EnterSpace
On the bell, opens the panel and moves focus into it, or closes it. On a row, activates it; on its dismiss control, removes the row and moves focus to the same control on the row that takes its place, or to the bell once the list is empty.
Tab
Moves through the panel: Mark all read, the two filters, each row and its dismiss control, then View all. Past View all it closes the panel with reason outside and carries on to the control after the bell; Shift+Tab before Mark all read closes it and lands on the bell.
Escape
Closes the panel and returns focus to the bell. Inside a dialog the panel takes the key first and the dialog stays.
API
<sui-notification-center>
Properties
Property
Attribute
Type
Default
Description
align
align
"end" | "start"
'end'
Which edge of the bell the panel lines up with, in reading order: end (the default) is the bell's right edge on a left-to-right page and its left edge under dir="rtl", so a bell at the end of a top bar opens its panel inward either way. The panel is fixed 8px under the bell and flips above when the viewport has no room.
listMaxHeight
list-max-height
number
300
Scrollable list height cap in px.
notifications
property only
SuiNotificationItem[]
[]
Inbox rows. Property, not attribute. Replacing the array resets read and dismissed state. Unread rows sit on the sunken surface with the orange dot; hover or keyboard focus reveals the dismiss control.
open
open
boolean
false
Shows the panel (the reference defaultOpen). Mutable; the element opens and closes itself, so plain HTML works without listeners. Opening by interaction moves focus into the panel; an outside pointer closes it without moving focus.
Fires before the panel closes itself; detail is 'trigger' | 'outside' | 'escape'. Cancelable: preventDefault() keeps it open. It does not bubble.
suiItemPick
SuiNotificationItem
onSuiItemPick
@sui-item-pick
Fires every time a row is activated, whatever its read state; detail is the item as the user saw it (so an unread row still reads read: false). This is the navigate signal, and activating also marks the row read. suiRead only reports that state change, and says nothing on an already-read row.
suiOpen
void
onSuiOpen
@sui-open
Fires when an interaction opens the panel. It does not bubble.
suiRead
SuiNotificationItem
onSuiRead
@sui-read
Fires when a row becomes read (row activation or "Mark all read"); detail is the item.
suiRemove
SuiNotificationItem
onSuiRemove
@sui-remove
Fires when a row's remove control is used; detail is the removed item. Focus moves to the same control on the row that takes its place, so Enter clears an inbox row by row, and to the bell once the list is empty. Named suiRemove because that is what the library calls this everywhere else. Dismissal has two shapes here and only two: suiClose carries a SuiCloseReason and says why something shut, and suiRemove carries the thing that went. This used to be suiDismiss, which was the only third name in the library, on an event that takes a row out of a list and hands you the row: the suiRemove shape exactly, as on sui-file-upload.
suiViewAll
void
onSuiViewAll
@sui-view-all
Fires when "View all notifications" is activated. No detail.
Methods
Method
Signature
Description
setFocus()
setFocus() => Promise<void>
Moves focus to the bell, the element's tab stop; it does not open the panel. The host's own focus() does the same.
Types
SuiCloseReasontypeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiKeytypeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiNotificationIteminterfaceOne inbox row. Severities follow the toast system; read rows lose the sunken ground and dot.
id?
SuiKey
Stable identity; defaults to the array index. A number stays a number in every event.
title
string
Sentence-case, single-sentence headline.
detail?
string
Supporting line under the title.
time?
string
Relative or clock time, e.g. "11:02 AM" or "Yesterday".
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.