Skip to content

Notifications · Concept

The Notifications API: system notifications

Published Updated

In one line: the Notifications API allows web pages to control the display of system notifications to the end user. A web notification is a message box rendered by the operating system’s own native notification system, so it displays identically to notifications from any other app on the platform — and because the OS renders it, it sits outside the top-level browsing context viewport and can be shown even when the user has switched tabs or moved to a different app. It is the display half of notifications; Web Push is the delivery half that wakes your service worker when the page is closed.

The API is available only in secure contexts (HTTPS), in some or all supporting browsers, and it is available in Web Workers.

  • Notifications API — creates and shows a notification. The interface is exposed to both Window and Worker, and the constructor is prohibited only in ServiceWorkerGlobalScope, so a non-persistent notification can come from a page or from another worker context; a service worker uses showNotification() instead.
  • Web Push — lets a server deliver a message to the user’s device even when the site is closed; the browser wakes the service worker, which then calls the Notifications API to show something. The division of labour is the thing to remember: the Notifications API provides the display, while Web Push enables server-initiated delivery when the app is closed.

Use the Notifications API directly for in-session alerts; pair it with Web Push for messages that must arrive when the app isn’t open.

Persistent vs non-persistent notifications

Section titled “Persistent vs non-persistent notifications”

The spec draws the line by service worker registration: a non-persistent notification is one whose service worker registration is null, and a persistent notification is one whose registration is non-null. That single distinction decides almost everything else about how a notification behaves.

Non-persistent Persistent
Created by new Notification(title, options) registration.showNotification(title, options)
Created in any scope the constructor is allowed in — Window or a non-service-worker Worker; MDN describes the common case as a browsing context such as a web page or tab a Window or Worker context with a ServiceWorkerRegistration
Lifetime tied to the creating context; MDN’s wording is that if the page is closed the notification can no longer be interacted with can remain interactive beyond the lifetime of an individual page
Events show, click, close fired on the Notification object notificationclick and notificationclose fired on the ServiceWorkerGlobalScope
Notification center user agents should not display it in a platform’s “notification center” (if available) user agents should display it there
Action buttons the constructor throws a TypeError if options["actions"] is not empty supported (see the per-browser table below)
Represented by exactly one Notification object zero or more Notification objects

Two consequences are worth stating plainly:

  • Non-persistent notifications are transient by design. User agents should run the close steps for a non-persistent notification a couple of seconds after it was created, and should keep it out of the notification center; persistent notifications are the ones user agents should display there. These are should-level recommendations rather than absolute requirements, so treat them as the design intent: if a notification is meant to survive long enough for the user to come back to it, the persistent path is the one written for that.
  • On mobile, persistent is the portable choice. MDN’s guidance is direct: if your code needs to run on mobile devices then you must use persistent notifications, because the Notification() constructor will throw a TypeError on most mobile browsers. “Most” is the right word — browser-compat-data records Chrome for Android as always throwing, while Firefox for Android mirrors desktop Firefox, where the constructor is supported. Writing to the persistent path is what makes the code portable across that split.
// Persistent: the path that works across the mobile split above.
// `ready` is the intended "delay until a worker is active" helper — use it once you
// know this page registers one. It is the wrong tool in a *detection* path, where it
// never rejects and waits indefinitely; see "How to detect it at runtime" below.
async function notify(title, body) {
const registration = await navigator.serviceWorker.ready;
await registration.showNotification(title, { body, data: { url: '/inbox' } });
}

Handle activation in the service worker, not the page:

service-worker.js
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});

ServiceWorkerRegistration.getNotifications() returns a list of the notifications in the order they were created from the current origin via the current service worker registration — useful for coalescing or clearing what you have already shown.

Showing a notification requires the user to grant the current origin permission to display system notifications. Notification.permission is one of three strings:

  • granted — the user has explicitly granted permission for the current origin to display system notifications.

  • denied — the user has explicitly denied it.

  • default — the user decision is unknown; in this case the application will act as if permission was denied. Treat it as “cannot show anything yet” rather than as a guarantee that the user has never seen a prompt.

  • Read the current state with Notification.permission.

  • Request it with Notification.requestPermission(), which resolves to the resulting state.

  • Call it from a user gesture. MDN states the method should only be called when handling a user gesture, such as when handling a mouse click.

  • The choice persists. Once a choice has been made, the setting will generally persist for the current session — so after a denial you typically cannot re-prompt during that session.

btn.addEventListener('click', async () => {
// Guard first: on an iOS tab the interface is undefined, so reading it throws.
if (!('Notification' in window)) return;
const permission = await Notification.requestPermission();
if (permission === 'granted') {
await notify('You are all set', 'We will let you know when something happens.');
}
});

options commonly includes body, icon, badge, tag (to coalesce or replace notifications), and data (a payload for the click handler). Two more are worth knowing:

  • actions — action buttons. Notification.maxActions is a static getter whose steps are to return the maximum number of actions supported, so read it rather than assuming a count. Remember the constructor rejects a non-empty actions list with a TypeError.
  • navigate — a URL that will be opened if the notification is accepted. When a navigate URL is set, a non-persistent notification does not fire a click event; the user agent navigates to that URL instead.

MDN marks the Notifications API’s availability as limited — it is not Baseline, because it does not work in some of the most widely-used browsers. The per-browser picture from MDN’s browser-compat-data is uneven in a way that matters more than the version numbers:

Browser Notification since Caveat recorded in browser-compat-data
Chrome (desktop) 20 Since Chrome 49 notifications do not work in incognito mode
Chrome (Android) 42 Partial: a notification can only be sent from a service worker, and the constructor always throws a TypeError
Edge 14 —
Firefox 22 actions only from Firefox 152
Safari (macOS) 7 actions not supported
Safari (iOS/iPadOS) 16.4 Partial: the Notification interface is undefined unless the page is a web app saved to the Home Screen, whose manifest has a non-default display value; a notification can only be sent from a service worker
Samsung Internet 4.0 Partial: available only through service workers
Android WebView / iOS WebView not supported —

The iOS/iPadOS row is the one that surprises people, and it lines up with WebKit’s own announcement: Web Push — which drives notifications — arrived for web apps added to the Home Screen in iOS and iPadOS 16.4, not for a site open in a Safari tab. See iOS and Safari Web Push for that platform’s rules.

Support for actions and navigate lags the base interface: actions landed in Chrome 53, Edge 18, Opera 39 and Firefox 152 and is not supported in Safari, while navigate is supported in Safari 18.4 and not in Chrome.

Because the interface can be entirely undefined (an iOS tab) and because the constructor can throw even where the interface exists (Chrome on Android), probe for the persistent path and branch explicitly when it is missing — do not assume a fallback to new Notification().

One detail decides whether the fallback branch ever runs: 'serviceWorker' in navigator proves only that the API exists, not that this page has a usable worker. ServiceWorkerContainer.ready is documented as a way of delaying code execution until a service worker is active — MDN’s contract is a promise that will never reject and that waits indefinitely until the registration associated with the page has an active worker. Awaiting it in a probe therefore has no bounded outcome: with nothing to activate, it neither fulfills nor rejects and the in-page fallback never executes.

getRegistration() is the settling alternative — MDN describes it as resolving to a ServiceWorkerRegistration or to undefined — but a registration on its own is not enough. ServiceWorkerRegistration.active returns the worker whose state is activating or activated, and MDN documents it as null when there is none; a registration can hold only an installing or waiting worker, and per the Service Worker specification an installing worker can end up redundant if installation fails, in which case no active worker ever appears. So check for the active worker directly and return null when it is absent, rather than handing the wait off to ready:

async function getNotifier() {
if (!('Notification' in window) || !('serviceWorker' in navigator)) {
// No usable Notifications API — e.g. an iOS tab that is not a Home Screen web app.
return null;
}
// getRegistration() settles either way; `ready` never rejects and waits indefinitely.
const registration = await navigator.serviceWorker.getRegistration();
// A registration may hold only an installing/waiting worker — and that install can
// end up redundant — so require the active one instead of awaiting activation here.
if (!registration || !registration.active) {
return null;
}
if (!('showNotification' in registration)) {
return null;
}
return registration;
}
async function alertUser(title, body) {
const notifier = await getNotifier();
if (!notifier) {
// Fallback: keep the message in the page instead of dropping it.
showInAppBanner(title, body);
return;
}
if (Notification.permission !== 'granted') {
showInAppBanner(title, body);
return;
}
await notifier.showNotification(title, { body });
}
  • Feature-detect Notification and navigator.serviceWorker before using either, and keep an in-page fallback for the branch where neither is there.
  • In a detection path, resolve navigator.serviceWorker.getRegistration() and require registration.active; do not await navigator.serviceWorker.ready, which never rejects and waits indefinitely for an active worker that an installing/waiting-only (or failed) registration may never produce. If you genuinely must wait for activation, watch the worker’s statechange for activated versus redundant behind your own timeout, and fall back when it expires.
  • Prefer registration.showNotification() everywhere — it is the portable mobile path, the one user agents should surface in the notification center, and the only one that accepts actions.
  • Never pass actions to new Notification(): a non-empty list throws a TypeError.
  • Read Notification.maxActions instead of hard-coding how many buttons you can show.
  • Request permission from a user gesture, in context, after the user opts into alerts.
  • After a denial, hide the feature rather than re-prompting during the session — the setting generally persists for it.
  • Handle clicks in the service worker’s notificationclick event; focus or open the right page.
  • On iOS and iPadOS, gate the whole feature behind a Home Screen web app whose manifest sets a non-default display value.
  • Do not ship notifications as a WebView feature: Android and iOS WebViews do not support the interface.
  • Pair with Web Push when notifications must arrive while the app is closed.

Specifications

SpecificationStatus
None.