To detect when a user switches away from your tab in JavaScript, listen for the visibilitychange event and check document.visibilityState. It is part of the Page Visibility API and works in every current browser.
TL;DR: Detect Tab Changes with JavaScript
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
console.log("Tab is hidden");
} else {
console.log("Tab is visible");
}
});This guide covers what does and doesn't count as leaving the tab, how it differs from blur and focus, and five practical uses: pausing video, keeping timers accurate, measuring active time, unread counts in the title, and sending analytics when the user leaves. It ends with a complete tab switch detector you can drop into a page.
Tested in: Chrome 152, October 2026.
What Counts as "Leaving the Tab"?
visibilitychange is about whether the page can be seen. blur and focus on window are about whether it has keyboard focus. They don't always fire together:
| What the user does | visibilitychange → hidden | window blur |
|---|---|---|
| Switches to another browser tab | Yes | Yes |
| Minimizes the browser window | Yes | Yes |
| Switches apps or locks the screen on a phone | Yes | Varies |
| Closes the tab or navigates away | Yes (Safari 14.1+) | — |
| Clicks another app placed next to the browser | No — the page is still visible | Yes |
| Clicks into an iframe on your page | No | Yes |
| Covers the browser with another window | Depends on the browser and OS | Usually |
So use visibilitychange for "can the user see this page?" and blur/focus for "is the user interacting with this window?".
Detecting Tab Switches with visibilitychange
document.visibilityState is "visible" or "hidden". The event fires on document every time it changes. document.hidden (a boolean) gives the same information; visibilityState reads more clearly and is what current documentation uses.
You don't need feature detection or vendor prefixes any more: Chrome 33+, Firefox 18+ and Safari 7+ support the unprefixed API.
visibilitychange vs blur and focus
To see both signals, log them side by side:
document.addEventListener("visibilitychange", () => {
console.log("visibility:", document.visibilityState);
});
window.addEventListener("blur", () => console.log("window lost focus"));
window.addEventListener("focus", () => console.log("window got focus"));Switching tabs logs both a visibility change and a blur. Clicking a window next to the browser logs only the blur. You can also check the current state at any time with document.hasFocus().
Real-World Uses
1. Pause and Resume a Video
const video = document.getElementById("myVideo");
let resumeOnReturn = false;
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
resumeOnReturn = !video.paused; // only resume what we paused
video.pause();
} else if (resumeOnReturn) {
video.play().catch(() => {}); // play() can be blocked by autoplay rules
}
});- Remember whether the video was playing before you paused it. Otherwise returning to the tab starts a video the user had paused on purpose.
- play() returns a promise that rejects when the browser blocks playback (for example, unmuted autoplay). Catch it, or you get an "Uncaught (in promise)" error.
2. Keep a Clock Accurate in Background Tabs
const display = document.getElementById("elapsed");
const startedAt = Date.now();
let intervalId = null;
function tick() {
// Read the real clock instead of counting ticks:
// background tabs throttle timers, so ticks get skipped
const seconds = Math.floor((Date.now() - startedAt) / 1000);
display.textContent = `${seconds} s`;
}
function startTicking() {
if (intervalId === null) intervalId = setInterval(tick, 1000);
tick();
}
function stopTicking() {
clearInterval(intervalId);
intervalId = null;
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") stopTicking();
else startTicking();
});
startTicking();Browsers slow down timers in background tabs to save battery, and requestAnimationFrame stops completely while a page is hidden — in our test it ran zero times in three seconds. Counting setInterval ticks therefore drifts. Read the real clock (Date.now()) on every update, stop the interval while hidden, and the display is correct the moment the user returns. Note the clock also starts on page load, not only after the first tab switch.
3. Measure Active Time on the Page
let activeMs = 0;
let visibleSince = document.visibilityState === "visible" ? performance.now() : null;
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
if (visibleSince !== null) activeMs += performance.now() - visibleSince;
visibleSince = null;
} else {
visibleSince = performance.now();
}
});
function getActiveSeconds() {
const current = visibleSince === null ? 0 : performance.now() - visibleSince;
return (activeMs + current) / 1000;
}Time only accumulates while the page is visible. performance.now() is used because it can't jump when the system clock changes.
4. Show Unread Messages in the Tab Title
const baseTitle = document.title;
let unread = 0;
function onNewMessage() {
if (document.visibilityState === "hidden") {
unread++;
document.title = `(${unread}) ${baseTitle}`;
}
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
unread = 0;
document.title = baseTitle;
}
});Call onNewMessage() from your chat or notification code. While the tab is hidden the title becomes "(2) My App"; it resets as soon as the user comes back.
5. Send Analytics When the User Leaves
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
const data = JSON.stringify({
page: location.pathname,
activeSeconds: Math.round(getActiveSeconds()),
});
navigator.sendBeacon("/analytics", data);
}
});- navigator.sendBeacon queues the request so the browser delivers it even if the page is closing. A normal fetch can be cancelled.
- Use visibilitychange, not unload or beforeunload: mobile browsers often never fire unload (the app is just killed in the background), and unload listeners can stop the page from using the back/forward cache.
- If you support Safari older than 14.1, also listen for pagehide, because those versions don't fire visibilitychange when the page is closed.
- This snippet uses getActiveSeconds() from use 3.
Complete Example: Tab Switch Detector
A self-contained page that shows whether it's visible, counts how many times you left, adds up the time away, and logs focus changes separately. Save it as an .html file and open it in your browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tab Switch Detector</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 520px; margin: 2rem auto; padding: 0 16px; }
dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; }
dt { font-weight: 600; }
dd { margin: 0; }
ol { padding-left: 20px; color: #444; }
</style>
</head>
<body>
<h1>Tab Switch Detector</h1>
<p>Switch to another tab or app, then come back.</p>
<dl>
<dt>Status</dt><dd id="status"></dd>
<dt>Times you left</dt><dd id="count">0</dd>
<dt>Time away</dt><dd id="away">0.0 s</dd>
</dl>
<ol id="log"></ol>
<script>
const statusEl = document.getElementById("status");
const countEl = document.getElementById("count");
const awayEl = document.getElementById("away");
const logEl = document.getElementById("log");
let leaves = 0;
let awayMs = 0;
let hiddenAt = null; // set when the user leaves, not if the page opened in a background tab
function log(text) {
const li = document.createElement("li");
li.textContent = `${new Date().toLocaleTimeString()} — ${text}`;
logEl.prepend(li);
}
function render() {
statusEl.textContent = document.visibilityState === "hidden" ? "Hidden" : "Visible";
countEl.textContent = leaves;
awayEl.textContent = `${(awayMs / 1000).toFixed(1)} s`;
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
leaves++;
hiddenAt = performance.now();
log("Left the tab");
} else if (hiddenAt !== null) {
const away = performance.now() - hiddenAt;
awayMs += away;
hiddenAt = null;
log(`Came back after ${(away / 1000).toFixed(1)} s`);
}
render();
});
// Focus changes without the tab being hidden, e.g. clicking another
// window that sits next to the browser
window.addEventListener("blur", () => log("Window lost focus"));
window.addEventListener("focus", () => log("Window got focus"));
render();
</script>
</body>
</html>A page that opens in a background tab (for example with a middle-click) isn't counted as "left" when the user first looks at it — only switching away after viewing it counts.
Using Tab Detection in Online Quizzes and Exams
A common reason to detect tab changes is flagging when a test-taker leaves the quiz page. The counter above is the core of that. Before relying on it, know what it can't see:
- A second device or a phone next to the keyboard.
- Another app or window placed beside the browser — the page stays visible; at most you get a blur.
- Accidental switches: notifications, the OS, or a browser prompt can trigger it too.
- Anything the user changes in their own browser: extensions and DevTools can block or fake these events.
Treat a switch count as a signal to review, not proof of cheating, and tell users up front that leaving the page is recorded. If you record it, send it to your server (for example with sendBeacon) — a counter that only lives in the page is easy to reset with a reload.
Browser Support
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
| Page Visibility API (visibilityState, hidden, visibilitychange) | 33 | 18 | 7 |
| visibilitychange when the page is closed | Yes | Yes | 14.1 |
| navigator.sendBeacon | 39 | 31 | 11.1 |
Versions are the first release with support, from MDN's browser compatibility data.
Common Mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Calling play() on every return | Videos the user paused start playing | Only resume what you paused |
| Starting the timer only inside the event handler | Nothing runs until the user switches tabs once | Start it on load too |
| Counting setInterval ticks | Time drifts in background tabs | Compute elapsed time from the clock |
| Sending data in unload | Often never sent on mobile | Use visibilitychange + sendBeacon |
| Using only blur | Fires for iframes and side-by-side windows, misses phone app switches | Use visibilitychange for visibility |
| Checking typeof document.hidden or vendor prefixes | Extra code for browsers nobody uses | Use document.visibilityState directly |
FAQs
How do I detect when a user switches tabs in JavaScript?
Listen for visibilitychange on document and check whether document.visibilityState is "hidden" or "visible".
What is the difference between visibilitychange and blur?
visibilitychange tells you whether the page can be seen. blur tells you the window lost keyboard focus, which also happens when the user clicks another window or an iframe while your page is still visible.
Can I detect when the user closes the tab?
You get a final visibilitychange to "hidden" when the tab is closed or the user navigates away. Use it to save state or send data with sendBeacon. You can't reliably tell a close apart from a tab switch.
Can I prevent users from switching tabs?
No. Browsers don't let pages block tab switching. You can only detect it and react.
Does visibilitychange work on mobile?
Yes. It fires when the user switches apps, goes to the home screen, or locks the phone. It's the most reliable "user left" signal on mobile.
Why does my timer run slowly when the tab is in the background?
Browsers throttle timers in background tabs and pause requestAnimationFrame entirely. Compute elapsed time from Date.now() or performance.now() instead of counting ticks.
How do I count tab switches during an online test?
Increment a counter each time visibilityState becomes "hidden" and report it to your server. The complete example above does the counting; read the quiz section for what it can't detect.
Related Articles
View allAbout Muhaymin Bin Mehmood
Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.




