How Browsers Decide What "Visible" Means: A Deep Dive Into the Page Visibility API
One of the small but important problems in web development is knowing whether the user is actually looking at your page. The browser does not normally tell you. A page that is open but in a background tab still runs JavaScript, still receives setInterval callbacks, still loads images, and still appears, from the script's point of view, to be completely alive. From the user's point of view, however, that page might not have been seen for hours.
This gap between "the page is loaded" and "the page is being seen" matters in many contexts. A music player that pauses when you switch tabs needs to know. A news site that wants to count an impression only when the article is actually on screen needs to know. A real-time dashboard that should slow its polling to save battery while no one is watching needs to know. A video advertisement that should pause its countdown when the viewer alt-tabs away needs to know.
The mechanism that solves this problem is the Page Visibility API, formalised by the World Wide Web Consortium and implemented in every major browser since around 2012. It is, on the surface, one of the simpler browser APIs. The full specification is short. There are two properties and one event. But the actual behaviour is more nuanced than the documentation suggests, and small differences between browsers can produce surprising results in production.
This article walks through how the API works, what "visible" actually means in each major browser, the edge cases that regularly trip up developers, and the patterns that have emerged for using the API reliably.
The basic interface
The Page Visibility API exposes two properties on the document object and one event on the document.
The first property is document.hidden, a boolean that is true if the document is currently hidden, and false otherwise. The second is document.visibilityState, a string that takes one of four values: "visible" (the page is in the foreground tab of a focused window), "hidden" (the page is not in a foreground tab, or the entire window is minimised, or the device is locked), "prerender" (the page is being prepared in the background but not yet shown), or, in some browsers and contexts, "unloaded" (the page is being torn down).
The event is visibilitychange, fired on the document whenever the visibility state changes. A typical use looks like this:
document.addEventListener("visibilitychange", () => {
if (document.hidden) pause();
else resume();
});
This pattern is used by media players, polling loops, animation controllers, and many other components that benefit from being aware of whether they are being watched.
What "visible" actually means
The interesting question is what the browser counts as "visible". The W3C specification gives a deliberately loose answer: a page is visible if it is "at least partially visible on at least one screen". This leaves implementation latitude, and the latitude has been used differently by different browser vendors over time.
In Chrome and Edge, a page in a foreground tab of a focused window is visible. A page in a background tab is hidden. A page in a foreground tab of an unfocused window — for example, you have your browser open behind another application — is generally also visible, because the page is still painting to the screen, even if the user's attention is not on it. Switching to a different application without changing the browser tab does not generally trigger a visibility change.
In Safari on macOS, the behaviour is slightly different. Safari has historically been more aggressive about marking pages as hidden when the browser window loses focus, treating an unfocused window as hidden even if the page is technically still painting. This produces different signals from the same user behaviour: the same "alt-tab away from the browser" action triggers a visibility change in Safari but does not in Chrome.
In Firefox, the behaviour falls between these two. Firefox marks a page as hidden when the tab is in the background, but not when the window loses focus. It does, however, throttle background tabs much more aggressively than Chrome, which produces different timing characteristics for any code that depends on background execution.
On mobile, the behaviour converges. All three major mobile browsers — Chrome on Android, Safari on iOS, and Firefox on Android — treat a page as hidden when the user navigates away to another app, when the device screen locks, or when the user switches to another browser tab. The "unfocused window" case does not really exist on mobile, since mobile browsers are generally fullscreen.
Why this matters for measurement
If you are building a feature that counts how long a user has been actively viewing a page, you need to decide what "active" means. The Page Visibility API is the standard primitive, but it does not give you a single answer. It gives you a stream of state changes that you have to integrate over time, and the answer you get will differ slightly between browsers.
The simplest pattern is to maintain a running counter that increments only while document.visibilityState === "visible", pausing when the state changes to "hidden" and resuming when it changes back. This produces a measurement of cumulative active visible time. It correctly handles tab switches, window minimisation, and (on mobile) backgrounding.
It does not, however, handle every case. A user who is looking at the page but is not interacting with it — perhaps reading a long article — will register as continuously visible even if they have looked away. A user who has the page open in a foreground tab but is in a different application will, in Chrome, register as visible even though they cannot see the page. A user with multiple monitors who has dragged the browser to a screen they are not currently looking at will register as visible.
For most measurement purposes, these edge cases are tolerable. The API gives you a reasonable approximation of attention, not a perfect one. For applications where the approximation is not good enough, additional signals are available — focus events, mouse movement, keyboard activity, scroll position — that can be combined with visibility to produce a richer estimate. Each of these additional signals has its own edge cases and its own privacy considerations.
Edge cases that catch people out
Several edge cases have repeatedly caught developers out, and are worth being aware of.
The first is iframes. An iframe inside a page does not have its own independent visibility state in the way you might expect. The visibility of the iframe's document follows the visibility of the parent page in most browsers. If you have JavaScript running inside an iframe and you check document.hidden, you will get the parent page's visibility, not the iframe's individual visibility. This matters for any analytics or measurement code embedded in an iframe — for example, a third-party advertising script.
The second is the prerender state. Some browsers prerender pages that they think the user is likely to navigate to next, executing the page's JavaScript in the background to make the navigation feel instant when it happens. During prerender, visibilityState is "prerender", not "visible". Code that triggers expensive operations on page load — for example, video playback or analytics impression tracking — should generally check for prerender and defer those operations until the page actually becomes visible.
The third is the locked-screen case on mobile. Different mobile browsers handle screen locking differently. Some fire a visibilitychange event with visibilityState === "hidden" when the screen locks. Others do not, and only fire the event when the user explicitly switches apps. If your code depends on knowing that the user has looked away, you cannot rely on the locked-screen case being signalled consistently.
The fourth is the page-bfcache case. Modern browsers use a "back-forward cache" that snapshots a page when the user navigates away, and restores the snapshot if the user navigates back. The page's JavaScript is paused, not unloaded. When the user navigates back, the page resumes from its snapshot state. Visibility events fire as expected during this dance, but the absolute timestamps you have stored in your code will be from the previous session, not the current one. Code that depends on time-since-page-load needs to be aware of this.
The fifth is automated-test environments. Headless browsers used for testing do not always implement the Page Visibility API consistently. Code that depends on the API to behave a certain way in tests should mock the API rather than rely on the test browser's native behaviour.
The privacy dimension
The Page Visibility API has been a subject of ongoing privacy discussion. On one hand, the API is a general primitive that lets web pages be polite about resource use — pausing animations, deferring polling, freeing memory — when they are not being seen. This is unambiguously good. On the other hand, the same primitive lets pages know when the user has switched tabs, which can be used to track engagement at a level of detail that some users would consider intrusive.
The current consensus is that visibility information itself is not particularly sensitive — every page has always been able to detect at least the focused/unfocused state through other means — and the convenience of having a single, well-defined API outweighs the marginal privacy cost. But the API is one of several signals that, taken together, can be used to construct a behavioural profile of a user, and developers building measurement systems should think about whether the granularity they are collecting is genuinely necessary for their purpose.
The most privacy-respectful pattern is to use the API only for its intended purpose — pausing work that does not need to happen while the page is unseen — and to record only aggregate measurements rather than fine-grained event-by-event traces. A counter of cumulative visible seconds is much less sensitive than a log of every time the user switched tabs.
Where this matters in practice
The Page Visibility API is a quiet workhorse. It is used by every major video service to pause playback when the user switches tabs. It is used by analytics platforms to filter out impressions that occurred in background tabs. It is used by polling-based applications to reduce their request frequency when no one is watching. It is used by collaboration tools to know when a user is actively present.
It is also the foundational primitive for any system that tries to measure attention to advertising in a way that resists basic gaming. A pure timer that runs from page load is trivially defeated by leaving a hidden tab open. A timer that uses the Page Visibility API to pause when the page is not visible at least requires the gamer to keep the page in the foreground, which raises the cost of fraud meaningfully even if it does not eliminate it entirely.
This is the reason the API features prominently in our own architecture at Add2Coin, where it forms part of the measured engagement criterion required to qualify an impression for attribution. The fuller technical context for that use is described in our whitepaper, but the API itself is general-purpose and well worth understanding for any web developer building anything that depends on knowing when the user is actually present.
Further reading
The W3C Page Visibility specification is short and readable: it is the authoritative reference for what each visibility state means. The Mozilla Developer Network reference is a good practical companion, with examples for the common patterns. The browser-vendor implementation notes — Chromium's, WebKit's, and Mozilla's — explain the small differences between browser behaviours. Anyone planning to use the API in production should read at least the spec and the MDN page; planning around the vendor differences usually requires testing in each target browser as well.
For most applications the API is straightforward to use. The complexity is not in calling it but in deciding what to do with the answer it gives, and that decision depends entirely on what you are trying to measure and why.