Notification API with Page Visibility API
When writing with Javascipt for the web, there are many APIs available for us to use. Some are very mature such as DocumentFragment while some are more experimental (yet still usable) like WebGL. In this article we combine two of these APIs to make a feature that many websites implement, sending messages to the user using the native browser notification (not to be confused with push notifications).
Notification API #
The image above is a simple example of a native browser notification. In the example, we use the title
, body
, and icon
properties.
Properties:
- title is the string ‘Hi i\’m Chris!’ (this is required)
- body is the string ‘Notification API with Page Visibility API’
- icon is the url string for the icon
Create Notification #
const title = 'Hi i\'m Chris!';
const body = 'Notification API with Page Visibility API';
const icon = '/icon.ico';
const options = {body, icon};
const notification = new Notification(title, options);
But wait, can the user just start spamming notifications? Short answer: no. You would need to first request the user’s permission using the call requestPermission()
and then once approved, calls for new Notification()
would generate a notification instance (and alert).
Notification Permission #
function callback(result) {
...
}
const promise = Notification.requestPermission(callback);
promise
.then((result) => {
...
}).catch((result) => {
...
});
The argument result
used in the callback functions above is based on what how the user interacts with the prompt popup.
default
when the user clicks dismisses the promptgranted
when the user clicks on Allowdenied
when the user clicks on Block
Both the callback and the promise will be resolved once the user interacts with the popup prompt.
Actions, Events, and Behaviour #
So cool, you can now send a notification to your users. But now your UX will suffer because of you did not handle user interactions with the notification like the following:
- Clicking on the notification
- Not timing out on certain browsers
- Become headless when the window or tab that originally launched the notification is now closed
- Spamming your users when they are actively using your site
Clicking on the notification #
Fortunately, the notification API provides us with a hook to manage this scenario. Simply pass a function to the onclick property of the notification object and it will fire once the user clicks on the notification.
const notification = new Notification(title, options);
notification.onclick = (event) => {
event.preventDefault();
window.focus();
event.currentTarget.close();
}
Use the
event.preventDefault()
call to stop the default behaviour of opening a new tab in the background when launching links rather than being focused on the new tab. However in our case, we are just closing the notification onclick.
This function assigned to the onclick
allows us to have reference back to the original window (and if you use scope changing methods like bind
, even have reference the current scope). Calling window.focus()
brings the user back into the site that launched the notification. Finally we close the notification dialog when the user clicks on it by using the Notification API given method close()
.
Timing out #
Some browsers automatically removes a notification after a certain period of time, but some take longer than that. To keep your UX consistent you should really take control of the browsers that take longer by closing the notifications if the users have not interacted with it. A simple timeout call when you create a new Notification will suffice.
const notification = new Notification(title, options);
const timeoutTime = 5000; // 5 seconds
setTimeout(() => {
notification.close();
}, timeoutTime);
Similarly to the onclick
method, we just close the dialog using the Notification API given close
method.
Being headless #
So in the rare occurrence that a notification is launched but either the user closes the page or the page closes, the notification dialog will lose reference to the page. Meaning stuff like window.focus()
or other bind-ed methods won’t work anymore and could result in an error.
To handle this, make sure to have an onDestroy
or similar method that will clean up any opened notifications and either change the onclick
behaviour or close it.
Keep all opened notifications in a data structure so you can reference them all later when cleaning up.
Spamming users #
We must make sure not to ask the user again for access to launch notifications when they have already denied it, or does not support it. Nicely, the requestPermission()
call returns the string denied
in the promise payload when the user has already denied the access. However if you have some UI to trigger the requestPermission()
call, this needs to be handled separately along with user-agents who don’t support notifications.
const supportsNotification = Boolean('Notification' in window);
const permissionAlreadyDenied = supportsNotification && Boolean(Notification.permission === 'denied');
const doNotShowUI = !!permissionAlreadyDenied;
Finally, even if we asked for the users’ permission that doesn’t mean we should keep alerting them using notifications. A good rule of thumb is to not send any notifications to the user when they are actively in the page. The Page Visibility API provides us data to whether the user is active in the page or not.
Page Visibility #
The Page Visibility API lets you know when a website is visible or in focus. When the user minimizes the webpage or moves to another tab, the API sends a visibilitychange event to let you know the visibility state of the website.
Historically, this detection has been done by registering onblur
and onfocus
handlers on the window. However onblur or onfocus does not handle when the page is not visible to the user rather than just not being focused upon.
const handleVisibilityChange = () => {
if (document[hidden]) {
// do actions for hidden page
} else {
// do actions for unhidden page
}
}
const visibilityChange = 'visibilitychange'; // might be prefixed
document.addEventListener(visibilityChange, handleVisibilityChange, false);
Visibility states of an
iframe
are the same as the parent document. Hiding the iframe with CSS properties does not trigger visibility events nor change the state of the content document.
Page Visibility Properties #
Document state is handled by the property document.hidden
which returns either true or false if the page is hidden (not visible) or not.
Different document.visibilityState
property states (or its prefixed version):
- visible: the page is a foreground tab of a non-minimized window
- hidden: the page is either a background tab or part of a minimized window, or the OS screen lock is active
- prerender: the page is being pre-rendered and is not visible, the page state out as this but will and cannot come back to this property (
document.hidden
will be true) (browser support optional) - unloaded: the page is being unloaded from memory (browser support optional)
Notification + Page Visibility and beyond #
So together the Notification API and the Page Visibility API brings a dynamic behaviour for alerting users of a completed event such as searches, processing completion, and etc.
One thing to keep note for both APIs is to make sure to have the prefixes built into your project since this is not currently supported (without prefixes) in some browsers.
To take it further we can:
- Use the Vibrate API to even get attention more when a new message comes
- Faveicon changes using
favicon.change('/newFavicon.ico')
when a new message comes - Altering and flashing (between the stored and the “new message”) the Document Page Title when a new message comes
- Keep track of Notification API unsupported properties when they become usable
- Polyfilling for usage in all working browsers
- Using Web Workers to handle notification events
- Finally using Push API after using Web Workers to further bring users back into your path
- If you want more, you can always try to convince Apple to let iOS devices send push notifications to give webapps and equal footing with native apps?
tldr; Use the Page Visibility API to conditionally show the native notification alerts when the page is not visible while doing everything that is possible to make native notifications unobtrusive to the user (and make sure to A/B test this feature in).