Learn how to Build a Tab Switch Detector in JavaScript

Muhaymin Bin Mehmood

Muhaymin Bin Mehmood

·
7 min read
Published Sep 26, 2024
Updated Oct 8, 2026
Learn How to Detect Tab Change in JavaScript Blog Banner Image
10 min read
1,048 words
22 sections8 code blocks

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

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 doesvisibilitychange → hiddenwindow blur
Switches to another browser tabYesYes
Minimizes the browser windowYesYes
Switches apps or locks the screen on a phoneYesVaries
Closes the tab or navigates awayYes (Safari 14.1+)—
Clicks another app placed next to the browserNo — the page is still visibleYes
Clicks into an iframe on your pageNoYes
Covers the browser with another windowDepends on the browser and OSUsually

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:

JavaScript
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

JavaScript
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

JavaScript
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

JavaScript
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

JavaScript
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

JavaScript
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.

HTML
<!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

FeatureChromeFirefoxSafari
Page Visibility API (visibilityState, hidden, visibilitychange)33187
visibilitychange when the page is closedYesYes14.1
navigator.sendBeacon393111.1

Versions are the first release with support, from MDN's browser compatibility data.

Common Mistakes

MistakeWhat happensFix
Calling play() on every returnVideos the user paused start playingOnly resume what you paused
Starting the timer only inside the event handlerNothing runs until the user switches tabs onceStart it on load too
Counting setInterval ticksTime drifts in background tabsCompute elapsed time from the clock
Sending data in unloadOften never sent on mobileUse visibilitychange + sendBeacon
Using only blurFires for iframes and side-by-side windows, misses phone app switchesUse visibilitychange for visibility
Checking typeof document.hidden or vendor prefixesExtra code for browsers nobody usesUse 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.

No comments yet. Be the first to comment!

Leave a Comment

2000 characters remaining

Related Articles

View all
Muhaymin Bin Mehmood

About 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.