Responding to notification dismissal
Beyond the notificationclick event, service workers can listen for the notificationclose event. This fires when a user dismisses a notification by clicking its close button or swiping it away, rather than clicking on the notification body itself.
This event is typically used for analytics, letting you track how users engage with your notifications. A basic listener looks like this:
self.addEventListener('notificationclose', function (event) {
const dismissedNotification = event.notification;
const promiseChain = notificationCloseAnalytics();
event.waitUntil(promiseChain);
});
Passing data with a notification
Push messages often carry data that only matters if the user interacts with the resulting notification—the destination URL is the most common example. The cleanest way to move that data from the push event to the notification is via the data property in the options object passed to showNotification().
const options = {
body:
'This notification has data attached to it that is printed ' +
"to the console when it's clicked.",
tag: 'data-notification',
data: {
time: new Date(Date.now()).toString(),
message: 'Hello, World!',
},
};
registration.showNotification('Notification with Data', options);
Later, inside a click handler, you can retrieve that value with event.notification.data.
const notificationData = event.notification.data;
console.log('');
console.log('The notification data has the following parameters:');
Object.keys(notificationData).forEach((key) => {
console.log(` ${key}: ${notificationData[key]}`);
});
console.log('');
Opening a window on click
Opening a specific URL is one of the most frequent responses to a notification click. The clients.openWindow() API handles this directly.
const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);
A better experience, though, is to focus an already-open tab instead of opening a duplicate. That's only possible for pages that belong to your own origin—service workers can see which of your site's tabs are open, but nothing about a user's activity elsewhere.
To focus an existing window, first normalize your target path into an absolute URL using the URL API so it can be compared reliably against the URLs of open tabs.
const urlToOpen = new URL(examplePage, self.location.origin).href;
Next, get all open window clients with clients.matchAll(). Two flags matter here: the search should only look for "window" type clients, and includeUncontrolled should be true to include tabs that aren't controlled by the current service worker.
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
Iterate through the returned clients and compare each one's URL to your target. On a match, focus that client. Wrapping this in a promise chain passed to event.waitUntil() keeps the service worker alive while the work completes. If no match is found, fall back to opening a new window.
const urlToOpen = new URL(examplePage, self.location.origin).href;
const promiseChain = clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
Collapsing notifications intelligently
Adding a tag to a notification replaces any existing notification with the same tag. That's a blunt instrument. The registration.getNotifications() API gives you finer control, allowing you to inspect all currently visible notifications and build more nuanced behaviors.
A chat application provides a good example. Instead of replacing the latest message, you might prefer to show "You have two messages from Matt." Suppose each notification carries a username in its data. Loop over the results of registration.getNotifications() until you find one matching the incoming message's sender.
const promiseChain = registration.getNotifications().then((notifications) => {
let currentNotification;
for (let i = 0; i < notifications.length; i++) {
if (notifications[i].data && notifications[i].data.userName === userName) {
currentNotification = notifications[i];
}
}
return currentNotification;
});
If a notification for that user is already displayed, increment a message counter in its data and update the title and body accordingly. Otherwise, create a new notification with a newMessageCount of 1.
.then((currentNotification) => {
let notificationTitle;
const options = {
icon: userIcon,
}
if (currentNotification) {
// We have an open notification, let's do something with it.
const messageCount = currentNotification.data.newMessageCount + 1;
options.body = `You have ${messageCount} new messages from ${userName}.`;
options.data = {
userName: userName,
newMessageCount: messageCount
};
notificationTitle = `New Messages from ${userName}`;
// Remember to close the old notification.
currentNotification.close();
} else {
options.body = `"${userMessage}"`;
options.data = {
userName: userName,
newMessageCount: 1
};
notificationTitle = `New Message from ${userName}`;
}
return registration.showNotification(
notificationTitle,
options
);
});
This produces a more cohesive notification trail:
- First message: A single notification from the user.
- Second message: The notification collapses into an aggregated "two messages" format.
Skipping the notification entirely
A push typically must show a notification, with one important exception: when the user already has your site open and focused. In that case, the push event can suppress the notification and instead notify the page directly.
First, determine whether any window client for your origin is focused:
function isClientFocused() {
return clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((windowClients) => {
let clientIsFocused = false;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.focused) {
clientIsFocused = true;
break;
}
}
return clientIsFocused;
});
}
Then use that check inside the push event to decide whether to display a notification.
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
console.log("Don't need to show a notification.");
return;
}
// Client isn't focused, we need to show a notification.
return self.registration.showNotification('Had to show a notification.');
});
event.waitUntil(promiseChain);
Posting a message to the page
When your site is already open and focused, a full notification can feel heavy-handed. An alternative is to send a message from the service worker to the page, letting the page itself decide how to surface the update.
After confirming a focused window exists, iterate through open clients and post a message to each:
const promiseChain = isClientFocused().then((clientIsFocused) => {
if (clientIsFocused) {
windowClients.forEach((windowClient) => {
windowClient.postMessage({
message: 'Received a push message.',
time: new Date().toString(),
});
});
} else {
return self.registration.showNotification('No focused windows', {
body: 'Had to show a notification instead of messaging each page.',
});
}
});
event.waitUntil(promiseChain);
On the page side, a standard message event listener picks it up:
navigator.serviceWorker.addEventListener('message', function (event) {
console.log('Received a message from service worker: ', event.data);
});
The listener can drive any UI you like. If a page doesn't register a listener, the messages are silently dropped—no harm done.
One further note: if your service worker intercepts fetch events, you can improve click-through UX by pre-caching the page and assets a user is likely to land on after interacting with a notification.



