Checking document visibility gives you insight into how visitors are interacting with your pages and can provide hints about the status of your applications. This post takes a look at what page visibility is, how you can use the Page Visibility API in your websites, and describes pitfalls to avoid if you build features around this functionality.
The Page Visibility API lets you find out the visibility of a page and set up event listeners to do something when page visibility changes. Let’s look at what page visibility means, how you can use this API, and some common pitfalls to avoid.
This post is also published on the MDN Blog if you’d like to check it out there!
# What’s the Page Visibility API?
The Page Visibility API was developed as a standalone specification, but it’s been incorporated directly into the HTML spec under Page visibility. This API exposes a way to determine a document’s visibility state so you can check if a web page is “visible” or “hidden”. Sounds straightforward, but pages can be “hidden” in several different ways.
A page is “hidden” if the browser window with the page is minimized, if it’s completely obscured by another application, or the user switches to another tab. Page visibility also changes to “hidden” if the operating system’s screen lock is activated, so mobile device behavior is also taken into consideration. By contrast, a page remains “visible” even when it’s partially visible on the screen.
# How to check page visibility changes
You can check the visibility of a document using document.visibilityState
, which will return either visible
or hidden
.
Alternatively, you can check the value of the document.hidden
Boolean property:
console.log(document.visibilityState); // "visible"
console.log(document.hidden); // false
In practice, it’s convenient to use the visibilitychange
event so you can trigger logic when the visibility state of a page changes instead of checking the visibility manually:
document.addEventListener("visibilitychange", (event) => {
// doSomething();
});
When the state changes, you can do something depending on the document.visibilityState
or document.hidden
Boolean:
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
// Page visibility is now 'hidden'
} else {
// Page visibility is now 'visible'
}
});
It’s worth noting that there’s no document.visible
, so if you’re only interested in that state, you can use document.visibilityState === "visible"
(or !document.hidden
):
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
// The page is visible again!
}
});
# Why page visibility is useful
Page visibility is useful in many cases, but the most likely fits are managing usage of compute resources, in analytics, and adding functionality that can improve user experience (UX) on a range of devices. Let’s take a look at these in more detail.
# Session lifecycle in analytics
In the analytics domain, it’s common to record the point when pages change from visible
to hidden
.
A page changing to a hidden
state might be the last event that’s observable by a page, so developers often treat it as the end of the user’s session.
However, this heavily depends on how you define ‘a session’.
You may define a session based on a fixed period of inactivity as opposed to relying solely on ‘first page hide’.
Therefore, this use case will vary according to your needs:
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
navigator.sendBeacon("/log", analyticsData);
}
};
# Managing resources efficiently
The use case I’m personally most excited about is the opportunity to stop doing something resource-intensive when someone is no longer viewing the page. This capability is really powerful considering that it allows us to manage client (or even server) compute or bandwidth in a deliberate way.
When it comes to managing resources, browsers already have some related features like tab unloading when system memory is critically low, and optimizing what background tabs are doing. We can still make our client-side applications more efficient, especially in areas that browsers don’t or can’t automatically handle.
For example, when a page is hidden, your app can use lower bitrate video, or throttle real-time communication (WebSockets and WebRTC). IndexedDB processes can be optimized as browsers don’t throttle these connections to avoid timeouts. You can stop invisible animations or other visual media, timers, throttle network requests, stop polling endpoints, there are a lot of opportunities for efficiency gains. You can find more information about what browsers do in the “Policies in place to aid background page performance” section on the Page Visibility API page.
# Improving user experience
When the page changes from hidden
to visible
, we could assume that a visitor has returned to our page, so we can restart anything that we may have paused when the page was hidden.
However, there are a few logical traps, especially concerning media, that you could fall into.
Therefore, you need to be careful when resuming actions in such cases.
We’ll look at examples of this in detail in the next section.
# Patterns to avoid with page visibility checks
User freedom is essential for your pages, so this API should be used with care to ensure your visitors have agency while browsing. In other words, give people control over when to start, resume, and skip media on your pages. You also shouldn’t assume that visitors want media to be paused automatically when a page is hidden. Expose a preference or allow visitors to opt-in to this kind of functionality instead.
There used to be a code snippet on MDN on the visibilitychange
page that was a good example of how this API can cause usability issues.
Can you spot what the problem is in the JavaScript code below?
<audio
controls
src="https://mdn.github.io/webaudio-examples/audio-basics/outfoxing.mp3"
></audio>
const audio = document.querySelector("audio");
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
audio.pause();
} else {
audio.play();
}
});
There is an <audio>
element with controls visible so the user can start and stop the media.
In JavaScript, we’re looking at the visibilitychange
event and doing the following:
- If the page is hidden, pause the audio.
- If the page is shown, play the audio.
This sounds reasonable, but there’s actually a big problem.
If I navigate to the page, then switch to another tab and back again (or restore a browser window after it’s been minimized) audio will autoplay.
The point is that page visibility may toggle between hidden
and visible
without the user ever interacting with the <audio>
element.
This situation becomes even more annoying if the <audio>
element is located below the fold and is not visible on page load.
This is extremely frustrating if visitors need to hunt down where audio is playing after they’ve located which tab it’s coming from.
To avoid usability issues, check if someone has interacted with media before resuming playback.
There may be better ways to approach this, but the fix I made to the MDN reference page example was to store the state of the audio player when the page is hidden.
When the page visibility changes to visible
, resume media playback only if it was playing when the page was hidden.
Let’s modify the event listener to store the audio state on page hide.
A boolean playingOnHide
is sufficient for this purpose; set it when the document’s visibility changes to hidden
:
// Initialize as false; we're not auto playing audio when the page loads
let playingOnHide = false;
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
// Page changed to "hidden" state, store if audio playing
playingOnHide = !audio.paused;
// Pause audio if page is hidden
audio.pause();
}
});
As with document.hidden
, there is no audio.playing
state, so we have to check !audio.paused
(not paused) instead.
That means the playingOnHide
Boolean can be set to whatever the value of !audio.paused
is on page hide, and we’re pretty much done.
The only other state the visibility of the page can be is visible
, so in the else
statement, we handle the playing logic:
let playingOnHide = false;
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
playingOnHide = !audio.paused;
audio.pause();
} else {
// Page became visible! Resume playing if audio was "playing on hide"
if (playingOnHide) {
audio.play();
}
}
});
The finished code looks like this with a tidy little gate for checking the audio state on page hide:
<audio
controls
src="https://mdn.github.io/webaudio-examples/audio-basics/outfoxing.mp3"
></audio>
const audio = document.querySelector("audio");
let playingOnHide = false;
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
playingOnHide = !audio.paused;
audio.pause();
} else {
if (playingOnHide) {
audio.play();
}
}
});
# That’s a wrap
I hope you enjoyed this post about page visibility and why I think it opens up interesting opportunities for resource management, analytics, UX, and other use cases. I’m looking forward to hearing your thoughts on different ways to use this API and what other efficiency gains developers can make use of that browsers don’t handle well.
Let me know what your thoughts are, if I missed something, or if you have any other suggestions. Feel free to get in touch on Bluesky if you have any feedback or if you want to say hi.
Published: