Notifications API
The Notifications API lets a web app display system-level notifications — the little banners that appear in the corner of the OS, outside the browser window. Combined with a service worker and the Push API, the same notification can be sent from a server and shown to the user even when the tab is closed. The browser surface is small, but the permission rules and UX considerations are everything.
Two flavours of notification
Page notifications — shown by your tab while it is open. Created with
new Notification(title, options).Service-worker notifications — shown via
registration.showNotification(title, options). Persist after the tab closes, can wake the SW, and are required for push.
Asking for permission
Notifications require explicit user consent. Notification.permission is one of "default" (not asked yet), "granted", or "denied".
if (Notification.permission === "default") {
const result = await Notification.requestPermission();
// result: "granted" | "denied" | "default"
}Showing a basic notification
if (Notification.permission === "granted") {
const n = new Notification("New message", {
body: "Alice replied in #general.",
icon: "/icons/chat.png",
badge: "/icons/badge.png",
tag: "msg-1234", // collapse duplicates
data: { url: "/chat/1234" },
});
n.addEventListener("click", () => {
window.focus();
location.assign(n.data.url);
n.close();
});
}The options worth knowing
body— secondary line of text.icon/badge— small images.badgeis a monochrome icon shown in some OS UIs (Android).tag— a string; a new notification with the same tag replaces the previous one instead of stacking.renotify— pair withtag. If true, replacing the previous notification still vibrates / sounds.silent— suppress the sound and vibration.requireInteraction— keep the notification visible until the user dismisses it (desktop only).actions— array of{ action, title, icon }buttons (service-worker notifications only).data— arbitrary payload accessible later from the click handler.
Service-worker notifications
from inside sw.js
self.registration.showNotification("Build finished", {
body: "main.js — 12.3 KB",
icon: "/icons/build.png",
actions: [
{ action: "open", title: "Open" },
{ action: "dismiss", title: "Dismiss" },
],
data: { url: "/builds/42" },
});handling the click
self.addEventListener("notificationclick", (event) => {
event.notification.close();
if (event.action === "dismiss") return;
event.waitUntil(
self.clients.matchAll({ type: "window" }).then((clients) => {
const open = clients.find((c) => c.url.endsWith(event.notification.data.url));
if (open) return open.focus();
return self.clients.openWindow(event.notification.data.url);
})
);
});The pattern — find an existing tab on the right URL and focus it, otherwise open a new one — is the standard UX for any notification that links somewhere.
Push notifications, in one page
The full flow has three actors:
Your service worker subscribes the browser to push:
registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }).Your server receives the resulting
PushSubscription(an endpoint URL and keys) and stores it.To notify, your server signs a push payload with the matching VAPID private key and POSTs it to the endpoint. The browser delivers it, fires
pushin the service worker, and your code callsshowNotification.
push.js (service worker)
self.addEventListener("push", (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title ?? "Update", {
body: data.body,
icon: data.icon,
})
);
});Notification vs other UI
In-page toast → use when the tab is open and focused. Cheaper and less intrusive.
System notification → use when the tab might be backgrounded, hidden, or closed.
Push notification → use when the server needs to reach the user without the tab being open at all.