Accessibility pattern · Communication

Notification center

The bell's name carries the unread count, and it opens a non-modal dialog: Escape closes it and returns focus, and Tab moves through and out of it as usual. New notifications raise the count and are announced once in a polite live region; they never take focus, and the demo's arrivals can be switched off.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Monsoon launch

Notifications

Show
  • Asha Rao mentioned you in Checkout copy

    Unread.

  • Vikram Shetty assigned you Pricing page review

    Unread.

  • Neha Kulkarni mentioned you in Launch checklist

    Unread.

  • Rohan Mehta commented on Hero image

  • Your export Q3 report.csv is ready

  • Meera Iyer shared Brand colors with you

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Enter or SpaceOn the bell, opens the panel and moves focus to its heading; pressed again, closes it.
Tab or ShiftTabMoves through the panel's buttons, filter and notifications, and on out of it; leaving the panel closes it.
Arrow Left or Arrow RightIn the filter, chooses All, Unread or Mentions.
Enter or SpaceOn a notification's button, marks it read or unread. Focus stays on the button.
EscapeCloses the panel and returns focus to the bell.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the bellNotifications, 3 unread, button, collapsed, opens a dialog
Enter opens the panelNotifications, dialog. Notifications, heading level 3
Tab reaches a notification's buttonMark as read Asha Rao mentioned you in Checkout copy, button
It is pressedMarked as read
A notification arrivesNew notification: Kabir Khan mentioned you in Release notes
Arrow Right in the filterUnread 3, radio button, checked, 2 of 3

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-notify" data-ap-notify>
  <div class="ap-notify__app">
    <div class="ap-notify__bar">
      <p class="ap-notify__brand"><span class="ap-notify__logo" aria-hidden="true"></span>Monsoon launch</p>
      <div class="ap-notify__tools">
        <button type="button" class="ap-notify__bell" data-ap-ntf-bell aria-haspopup="dialog" aria-expanded="false" aria-controls="ntf-panel" aria-label="Notifications, 3 unread">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 9a6 6 0 0 1 12 0c0 6 2.5 7.5 2.5 7.5h-17S6 15 6 9Z"/><path d="M10 20a2.2 2.2 0 0 0 4 0"/></svg>
          <span class="ap-notify__badge" data-ap-ntf-badge aria-hidden="true">3</span>
        </button>
        <span class="ap-notify__me" aria-hidden="true" translate="no">KP</span>

        <dialog class="ap-notify__panel" id="ntf-panel" aria-labelledby="ntf-name">
          <div class="ap-notify__head">
            <h3 class="ap-notify__name" id="ntf-name" tabindex="-1" autofocus>Notifications</h3>
            <button type="button" class="ap-notify__all" data-ap-ntf-all>Mark all as read</button>
          </div>
          <fieldset class="ap-notify__filter">
            <legend class="ap-notify__vh">Show</legend>
            <label class="ap-notify__opt"><input type="radio" name="ntf-filter" value="all" checked /><span>All</span></label>
            <label class="ap-notify__opt"><input type="radio" name="ntf-filter" value="unread" /><span>Unread</span> <span class="ap-notify__n" data-ap-ntf-unread>3</span></label>
            <label class="ap-notify__opt"><input type="radio" name="ntf-filter" value="mention" /><span>Mentions</span> <span class="ap-notify__n" data-ap-ntf-mentions>2</span></label>
          </fieldset>
          <ul class="ap-notify__list" data-ap-ntf-list>
      <li class="ap-notify__item" data-kind="mention" data-unread>
        <span class="ap-notify__dot" aria-hidden="true"></span>
        <span class="ap-notify__avatar" aria-hidden="true" translate="no">AR</span>
        <div class="ap-notify__body">
          <p class="ap-notify__text" id="ntf-1"><strong>Asha Rao</strong> mentioned you in <strong>Checkout copy</strong></p>
          <p class="ap-notify__meta"><span class="ap-notify__vh" data-ap-ntf-state>Unread.</span> <time datetime="2026-10-05T11:55+05:30">5 min ago</time></p>
        </div>
        <button type="button" class="ap-notify__mark" data-ap-ntf-mark aria-labelledby="ntf-1-act ntf-1">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.8 2.8L16 10"/></svg>
          <span id="ntf-1-act" hidden>Mark as read</span>
          <span class="ap-notify__tip" aria-hidden="true">Mark as read</span>
        </button>
      </li>
      <li class="ap-notify__item" data-kind="assign" data-unread>
        <span class="ap-notify__dot" aria-hidden="true"></span>
        <span class="ap-notify__avatar" aria-hidden="true" translate="no">VS</span>
        <div class="ap-notify__body">
          <p class="ap-notify__text" id="ntf-2"><strong>Vikram Shetty</strong> assigned you <strong>Pricing page review</strong></p>
          <p class="ap-notify__meta"><span class="ap-notify__vh" data-ap-ntf-state>Unread.</span> <time datetime="2026-10-05T11:38+05:30">22 min ago</time></p>
        </div>
        <button type="button" class="ap-notify__mark" data-ap-ntf-mark aria-labelledby="ntf-2-act ntf-2">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.8 2.8L16 10"/></svg>
          <span id="ntf-2-act" hidden>Mark as read</span>
          <span class="ap-notify__tip" aria-hidden="true">Mark as read</span>
        </button>
      </li>
      <li class="ap-notify__item" data-kind="mention" data-unread>
        <span class="ap-notify__dot" aria-hidden="true"></span>
        <span class="ap-notify__avatar" aria-hidden="true" translate="no">NK</span>
        <div class="ap-notify__body">
          <p class="ap-notify__text" id="ntf-3"><strong>Neha Kulkarni</strong> mentioned you in <strong>Launch checklist</strong></p>
          <p class="ap-notify__meta"><span class="ap-notify__vh" data-ap-ntf-state>Unread.</span> <time datetime="2026-10-05T11:00+05:30">1 hour ago</time></p>
        </div>
        <button type="button" class="ap-notify__mark" data-ap-ntf-mark aria-labelledby="ntf-3-act ntf-3">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.8 2.8L16 10"/></svg>
          <span id="ntf-3-act" hidden>Mark as read</span>
          <span class="ap-notify__tip" aria-hidden="true">Mark as read</span>
        </button>
      </li>
      <li class="ap-notify__item" data-kind="comment">
        <span class="ap-notify__dot" aria-hidden="true"></span>
        <span class="ap-notify__avatar" aria-hidden="true" translate="no">RM</span>
        <div class="ap-notify__body">
          <p class="ap-notify__text" id="ntf-4"><strong>Rohan Mehta</strong> commented on <strong>Hero image</strong></p>
          <p class="ap-notify__meta"><span class="ap-notify__vh" data-ap-ntf-state hidden>Unread.</span> <time datetime="2026-10-05T09:02+05:30">3 hours ago</time></p>
        </div>
        <button type="button" class="ap-notify__mark" data-ap-ntf-mark aria-labelledby="ntf-4-act ntf-4">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.8 2.8L16 10"/></svg>
          <span id="ntf-4-act" hidden>Mark as unread</span>
          <span class="ap-notify__tip" aria-hidden="true">Mark as unread</span>
        </button>
      </li>
      <li class="ap-notify__item" data-kind="system">
        <span class="ap-notify__dot" aria-hidden="true"></span>
        <span class="ap-notify__avatar ap-notify__avatar--sys" aria-hidden="true" translate="no">Q3</span>
        <div class="ap-notify__body">
          <p class="ap-notify__text" id="ntf-5">Your export <strong>Q3 report.csv</strong> is ready</p>
          <p class="ap-notify__meta"><span class="ap-notify__vh" data-ap-ntf-state hidden>Unread.</span> <time datetime="2026-10-04T16:10+05:30">20 hours ago</time></p>
        </div>
        <button type="button" class="ap-notify__mark" data-ap-ntf-mark aria-labelledby="ntf-5-act ntf-5">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.8 2.8L16 10"/></svg>
          <span id="ntf-5-act" hidden>Mark as unread</span>
          <span class="ap-notify__tip" aria-hidden="true">Mark as unread</span>
        </button>
      </li>
      <li class="ap-notify__item" data-kind="share">
        <span class="ap-notify__dot" aria-hidden="true"></span>
        <span class="ap-notify__avatar" aria-hidden="true" translate="no">MI</span>
        <div class="ap-notify__body">
          <p class="ap-notify__text" id="ntf-6"><strong>Meera Iyer</strong> shared <strong>Brand colors</strong> with you</p>
          <p class="ap-notify__meta"><span class="ap-notify__vh" data-ap-ntf-state hidden>Unread.</span> <time datetime="2026-10-03T14:30+05:30">2 days ago</time></p>
        </div>
        <button type="button" class="ap-notify__mark" data-ap-ntf-mark aria-labelledby="ntf-6-act ntf-6">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.8 2.8L16 10"/></svg>
          <span id="ntf-6-act" hidden>Mark as unread</span>
          <span class="ap-notify__tip" aria-hidden="true">Mark as unread</span>
        </button>
      </li>
          </ul>
          <p class="ap-notify__empty" data-ap-ntf-empty hidden></p>
        </dialog>
      </div>
    </div>
    <div class="ap-notify__page" aria-hidden="true">
      <span class="ap-notify__bone ap-notify__bone--h"></span>
      <span class="ap-notify__bone"></span>
      <span class="ap-notify__bone ap-notify__bone--s"></span>
      <span class="ap-notify__tiles"><span></span><span></span><span></span></span>
      <span class="ap-notify__rows"><span></span><span></span><span></span><span></span></span>
    </div>
  </div>

  <label class="ap-notify__sim">
    <input type="checkbox" role="switch" data-ap-ntf-sim checked />
    <span class="ap-notify__track" aria-hidden="true"></span>
    <span>Demo: a new notification every 15 seconds</span>
  </label>
  <p class="ap-notify__vh" aria-live="polite" data-ap-ntf-say></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The panel is a dialog named by its heading, notifications are a list, and the filter is a fieldset of radio buttons with a legend.

  • 1.4.1 Use of Color Level A

    Unread notifications have a dot and bold text, and the word Unread in text; color is never the only sign.

  • 2.1.1 Keyboard Level A

    Opening, filtering, marking read and closing all work from the keyboard.

  • 2.2.2 Pause, Stop, Hide Level A

    New notifications arrive on their own, so a switch stops them; in a product, that is a do-not-disturb setting.

  • 2.4.3 Focus Order Level A

    Opening moves focus to the panel's heading, and Escape returns it to the bell; arrivals never move it.

  • 4.1.2 Name, Role, Value Level A

    The bell has a name with the count, aria-expanded and aria-haspopup; each Mark as read button is named with the notification it acts on.

  • 4.1.3 Status Messages Level AA

    Each arrival and each change of read state is announced once through a polite live region.

Usage

When to use it

Use it

  • Apps that collect activity for people to catch up on: mentions, assignments, finished jobs.
  • Updates that can wait until people look, rather than ones that need action now.

Use something else

  • Something that needs an answer right away: use an alert or an alert dialog.
  • A one-off confirmation of what someone just did: a status message or a toast says it in place.
  • A long history people search and sort: give it a page with a table or a feed.

Common failures

How it usually goes wrong

  • A bell named only "Bell"

    An icon button with no name, or the name of the icon, says nothing about new activity. Here the name is Notifications, 3 unread, and it updates.

  • Unread shown by color alone

    A tinted row is invisible to many people and to every screen reader. Unread here is a dot, bold text and the word Unread.

  • Announcing arrivals over and over

    Rereading the whole list, or repeating "3 unread" every few seconds, buries everything else. Each arrival is announced once, in one sentence.

  • Arrivals that steal focus

    Moving focus to a new notification throws people out of what they were doing. Arrivals update the count and the list; focus stays where it was.

  • Buttons that all say Mark as read

    Out of context, six identical buttons are guesswork. Each one here is named with its notification.

  • Items that vanish from under focus

    In the Unread view, a notification marked read stays until the filter changes, so focus is never left on something that has gone.

Notes

Building it

  • The panel is a dialog opened with show(), not showModal(): the page stays usable, so Tab can leave it, and leaving closes it.
  • Each Mark as read button takes its name from two elements with aria-labelledby: a hidden span with the action and the notification's own text.
  • The badge is aria-hidden; the count reaches screen readers through the bell's name, which is the only place it needs to be.
  • Arrivals are announced from a live region outside the panel, because content inside a closed dialog is hidden and would not be read.
  • The count phrase in the bell's name (Notifications, 3 unread) is one string, so a translation can put the number where its grammar needs it.

Sources: WAI-ARIA Authoring Practices: Dialog (Modal) · HTML: the dialog element

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction