Notification Centres and Unread Counts
Permalink to "Notification Centres and Unread Counts"A notification centre is where job results, mentions, alerts and system messages live after their toasts have gone. It is what makes transient toasts acceptable, because nothing is lost. It is also a component with plenty of accessibility traps: a bell icon whose badge count is visible but not in the name, a panel that opens without moving focus, a list of unlabelled cards, and a count that changes silently — or noisily, on every incoming message.
This guide belongs to notifications & toasts for data operations.
Spec reference
Permalink to "Spec reference"- SC 4.1.2 Name, Role, Value (A) — the button’s name must include the unread count (“Notifications, 3 unread”); a visual badge alone is not programmatic.
- SC 1.1.1 Non-text Content (A) — the bell icon is decorative once the button has a text name.
- SC 1.3.1 Info and Relationships (A) — notifications are a list; each item’s time and status are part of its content.
- SC 2.4.3 Focus Order (A) — opening the centre moves focus into it; closing returns to the button.
- SC 4.1.3 Status Messages (AA) — count changes may be status messages; announce meaningful ones (an error arrived) rather than every increment.
- SC 2.5.3 Label in Name (A) — if the button shows the visible text “Notifications”, the accessible name must start with it.
When to use — and when not to
Permalink to "When to use — and when not to"Use a notification centre whenever the product emits more than occasional toasts — background jobs, collaboration events, scheduled reports. It is the accessible fallback for every timed toast.
Do not open it automatically when notifications arrive; that steals focus. Do not make it the only place errors appear for actions the user is currently performing — those belong in context.
The misapplication: a bell button named “Notifications” with a red “3” badge in a pseudo-element. Screen reader users never learn there is anything new.
Annotated code example
Permalink to "Annotated code example"<button type="button" id="notif-btn" aria-haspopup="dialog" aria-expanded="false" aria-controls="notif-panel">
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">…</svg> <!-- decorative (SC 1.1.1) -->
<span>Notifications</span> <!-- visible label (SC 2.5.3) -->
<span class="badge" id="notif-count">3<span class="visually-hidden"> unread</span></span>
</button>
<div id="notif-panel" role="dialog" aria-labelledby="notif-h" hidden> <!-- non-modal -->
<h2 id="notif-h" tabindex="-1">Notifications</h2>
<button type="button" id="mark-all">Mark all as read</button>
<ul class="notif-list">
<li class="unread">
<h3>Export ready: invoices.xlsx</h3>
<p><time datetime="2026-10-02T10:42">10:42</time> · <span>Unread</span></p> <!-- state as text -->
<a href="/exports/123">Download invoices.xlsx</a>
<button type="button">Mark “Export ready” as read</button>
</li>
</ul>
</div>
function setCount(n, { urgent } = {}) {
countEl.firstChild.nodeValue = String(n);
countEl.hidden = n === 0;
// Announce only urgent arrivals, not every increment (SC 4.1.3)
if (urgent) status.textContent = `New error notification: ${urgent}. ${n} unread.`;
}
markAll.addEventListener('click', () => {
markAllRead();
setCount(0);
status.textContent = 'All notifications marked as read.';
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected announcement | AT deviations |
|---|---|---|
| Tab to button | “Notifications 3 unread, button, collapsed” | — |
| Enter | Focus to heading: “Notifications, heading level 2” | Non-modal: Tab can leave the panel |
| Tab through list | Item headings and actions | VoiceOver reads list size on entry |
| Escape | Panel closes; focus to button | — |
| Error arrives while working | “New error notification: Report failed. 4 unread.” | — |
| Routine item arrives | Silent; count in button name updates | — |
Integration context
Permalink to "Integration context"Toasts that feed the centre are covered in accessible toast notifications for background jobs. The panel’s focus behaviour follows the non-modal option in record inspector panels: dialog or region?. Time stamps follow the time element and relative timestamps.
Gotchas
Permalink to "Gotchas"Counts above 99. “99+” is read as “99 plus” — fine — but the accessible name should say “more than 99 unread”.
Read state as colour. Bold or a blue dot for unread items is visual only; add text (“Unread”) or a visually hidden equivalent.
Infinite lists. Long histories should paginate or load more with an announced count, as in accessible load more versus infinite scroll.
Framework notes
Permalink to "Framework notes"In React, derive the button’s name from the same count state that renders the badge so they never diverge. In Angular, MatBadge adds aria-describedby with the badge text; set matBadgeDescription="3 unread" explicitly, since descriptions are read after the name. In Vue, compute the name string and bind it to a visually hidden span rather than aria-label, so the visible text stays part of the name.
<!-- Angular Material -->
<button mat-icon-button matBadge="3" matBadgeDescription="3 unread notifications" aria-label="Notifications">
<mat-icon aria-hidden="true">notifications</mat-icon>
</button>
Worked example
Permalink to "Worked example"A data platform sends three kinds of notification: job results, mentions in comments, and failures of scheduled reports. Before the redesign, the bell showed a red dot, its name was “Notifications”, and the panel was a div that opened without focus. After it, the button reads “Notifications, 4 unread” and announces only report failures as they arrive (“New error notification: Weekly revenue report failed. 5 unread.”). Opening the panel moves focus to its heading; each notification is a list item with a heading, a time and a text read state; and “Mark all as read” confirms itself. A screen reader user who ignores the bell for a morning can catch up in under a minute by opening the panel and pressing H to move from notification to notification.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Provide a NotificationCentre with a typed message model (kind, title, time, actions, read) shared with the toast API, so every toast is automatically recorded. Expose urgency from message kind, and keep the arrival announcement rules inside the component.
FAQ
Permalink to "FAQ"Should the notification panel be modal?
Usually not. Users often want to read a notification and act on the page behind it. A non-modal dialog with Escape to close and focus return is the common choice.
Should each notification have a heading?
Headings make long lists navigable with the H key. For short lists, a list of named items without headings is also fine.
Should the count include read items?
No. The count should be unread items only, and the name should say “unread”.
How do I announce a burst of notifications?
Summarise: “5 new notifications, 1 error”. Announcing each one separately overwhelms users.
Should the count update while the panel is open?
Yes, but silently; the user is already looking at the list. New items can be inserted at the top with a short polite message (“1 new notification”) if they arrive while the panel has focus.
Should notifications be grouped by day or type?
Grouping with headings (“Today”, “Earlier this week”) helps long lists, and each group heading becomes a jump target for screen reader users. Keep groups few and stable.
Related
Permalink to "Related"- Toasts for background jobs — where messages start
- Toast timing — why history matters
- Naming icon-only row action buttons — naming icon buttons