# Notifications API

> Showing system notifications with showNotification() and getNotifications(), the persistent versus non-persistent split, and the TypeError paths.

The Notifications API displays messages through the operating system's own notification
surface, outside the page, so they remain visible after the user switches tabs or apps. A PWA
uses it in two shapes: a persistent notification created by
`ServiceWorkerRegistration.showNotification()`, which outlives the page and is the only shape
mobile browsers allow, and a non-persistent one created by `new Notification()` in a page. This
entry covers the persistent path and the split; the constructor has
[its own entry](/reference/notifications/notification/).

## Syntax

```js
registration.showNotification(title)
registration.showNotification(title, options)
registration.getNotifications()
registration.getNotifications({ tag })
```

`showNotification()` returns a `Promise<void>` that fulfils once the notification is handed to the
OS; `getNotifications()` resolves with the `Notification` objects this registration has shown, in
creation order, optionally filtered by `tag`. Both are available in windows and in the service
worker itself (`self.registration`), and require a secure context (BCD
`api.ServiceWorkerRegistration.showNotification`: Chrome 42, Firefox 44, Safari 16 on macOS
Ventura, Safari 16.4 on iOS for Home Screen web apps).

## Parameters

| Parameter | Type | Meaning |
|---|---|---|
| `title` | string | The first line of the notification. Required. |
| `options.body` | string | Secondary text under the title. |
| `options.icon` | string (URL) | Large image beside the text. |
| `options.badge` | string (URL) | Small monochrome image used where the full notification does not fit, such as the Android status bar. Chromium only; see [actions and badge](/reference/notifications/notification-actions-badge/). |
| `options.image` | string (URL) | Large picture below the text. Chromium only. |
| `options.tag` | string | Identifier; a new notification with the same tag replaces the old one. |
| `options.renotify` | boolean | Alert the user again when replacing by `tag`. Requires a non-empty `tag`. Chromium only. |
| `options.requireInteraction` | boolean | Keep the notification on screen until the user acts. Chromium; Firefox 117 on Windows only. |
| `options.silent` | boolean | Suppress sound and vibration. Chrome 43, Firefox 132, Safari 16.6. Incompatible with `vibrate`. |
| `options.data` | any structured-cloneable value | Payload read back as `event.notification.data` in `notificationclick`. |
| `options.actions` | array | Buttons; persistent notifications only. See the actions entry. |
| `options.navigate` | string (URL) | Safari 18.4: open this URL on activation instead of firing `notificationclick`. |
| `options.timestamp`, `options.vibrate`, `options.dir`, `options.lang` | number, array, string, string | Display time, vibration pattern (Chromium on Android), text direction, BCP 47 language tag. |

A persistent notification has a non-null service worker registration; the specification uses
that single fact to decide the rest. Its `notificationclick` and `notificationclose` events fire
on the `ServiceWorkerGlobalScope`, it should appear in the platform's notification centre, and
it may carry `actions`. A non-persistent one fires `click` and `close` on its `Notification`
object, is closed by the browser a few seconds after display, and rejects `actions`.

## Exceptions

| Exception | When |
|---|---|
| `TypeError` | The registration has no worker in the `activating` or `activated` state. `navigator.serviceWorker.ready` resolves only when one exists, which is why examples await it. |
| `TypeError` | `Notification.permission` is not `"granted"`. Chromium's message is `Failed to execute 'showNotification' on 'ServiceWorkerRegistration': No notification permission has been granted for this origin.` ([service_worker_registration_notifications.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/notifications/service_worker_registration_notifications.cc), chromium.googlesource.com). |
| `TypeError` | `renotify` is `true` with an empty `tag`, or `silent` is `true` together with `vibrate`. |
| `DataCloneError` `DOMException` | `options.data` cannot be structured-cloned (a function, a DOM node). |

The promise rejects; nothing is thrown synchronously. A rejected `showNotification()` inside a
`push` handler also breaks the `userVisibleOnly` promise, which WebKit answers by revoking the
subscription, so handle these cases before the call rather than in a `catch`.

## Support by engine

Desktop support is complete: Chrome 42, Edge 17, Firefox 44, and Safari 16 on macOS Ventura.
Mobile is where the shape matters. Chrome for Android (42) and Samsung Internet expose
notifications only through a service worker and make the constructor throw; Safari on iOS and
iPadOS 16.4 exposes the interface only inside a Home Screen web app, and only through a service
worker; Android WebView has no implementation (BCD `api.Notification`). Chrome also disables
notifications in Incognito windows from version 49. Writing to the persistent path is therefore
the portable choice, not an optimisation.

## Examples

The examples split by where the code runs: the page shows and the service worker handles the click.

### Showing a persistent notification from the page

Wait for an active worker, then call `showNotification()` on the registration. The `data`
member carries what the click handler needs.

```js
async function notify(title, body, url) {
  const registration = await navigator.serviceWorker.ready;
  await registration.showNotification(title, {
    body,
    icon: '/icons/icon-192.png',
    tag: 'inbox',
    data: { url },
  });
}
```

`tag: 'inbox'` collapses repeated calls into one notification, which is what the user wants for
"3 new messages" and not what they want for three separate chat threads.

### Handling the click in the service worker

Persistent notifications fire `notificationclick` on the worker. Close the notification, then
focus an existing window if one is open at the target URL and open a new one otherwise.

```js
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const url = new URL(event.notification.data?.url ?? '/', self.location.origin).href;
  event.waitUntil((async () => {
    const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
    const open = windows.find((client) => client.url === url);
    if (open) return open.focus();
    return self.clients.openWindow(url);
  })());
});
```

`event.action` is the empty string when the body was clicked and the `action` id when a button
was; the handler above treats both the same.

### Detecting a usable notifier and falling back

Three things must hold: the interface exists, permission is granted, and the registration has
an active worker. Check each and return an in-page fallback otherwise, instead of awaiting
`ready`, which does not settle on a page with no registration.

```js
async function getNotifier() {
  if (!('Notification' in window) || !('serviceWorker' in navigator)) {
    return showInPageBanner; // iOS tab or embedded web view: no system notifications
  }
  const registration = await navigator.serviceWorker.getRegistration();
  if (!registration?.active || Notification.permission !== 'granted') {
    return showInPageBanner; // not yet installed, or permission not granted
  }
  return (title, body) => registration.showNotification(title, { body });
}
```

The returned function has the same signature in both branches, so callers do not care which
one they got.

:::observed
Calling `registration.showNotification('x')` in Chrome's Console while
`Notification.permission` is `"default"` or `"denied"` logs `Uncaught (in promise) TypeError:
Failed to execute 'showNotification' on 'ServiceWorkerRegistration': No notification permission
has been granted for this origin.`; the string is defined in Chromium's
[service_worker_registration_notifications.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/notifications/service_worker_registration_notifications.cc)
(chromium.googlesource.com). The same file holds the `renotify` and `silent` validation messages,
and DevTools, Application > Service workers, shows each displayed notification's `title` and
`tag` under the registration when the **Notifications** background service is being recorded.
:::

## See also

- [Notifications API Standard](https://notifications.spec.whatwg.org/) (whatwg.org)
- [ServiceWorkerRegistration: showNotification() method](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification) (developer.mozilla.org)
- [Notification interface](/reference/notifications/notification/)
- [Notification.requestPermission()](/reference/notifications/permissions/)
- [Web Push and PushManager.subscribe()](/reference/notifications/web-push/)
- [Notification actions and badge options](/reference/notifications/notification-actions-badge/)