What is the difference between the document `visibilitychange` event and the window `focus`/`blur` events, and when is a page visible but not focused?
answer
- pixels versus keystrokes
- side-by-side windows break the assumption
- DevTools in its own window
- one reader is synchronous
- three states, not two
basics
~20 sVisibility says whether the document is being presented at all; focus says whether it receives keyboard input. A page beside a focused second window, or with DevTools focused, stays visible while blurred. Use visibility to suspend work, focus for input-related behaviour.
solid answer
~50 sThey answer different questions. `visibilitychange` on `document` tracks whether the document is presented to the user at all: it goes `"hidden"` for a background tab, a minimised window, an app switch or a locked screen. `focus` and `blur` on `window` track whether the page is the active input target. The two diverge constantly — a page in a side-by-side window, or one whose user just clicked into DevTools or another application, is fully `"visible"` yet blurred, and it will keep animating and repainting because the user can see it. `document.hasFocus()` reads the focus half synchronously. The rule of thumb: suspend work that costs CPU when the page goes hidden, because nobody can see it; react to `blur` only for input-shaped concerns — pausing a typing tutor, dimming a caret, hiding a hover tooltip. Saving state belongs to the visibility signal, not the focus one.
go deeper
Know that these are two different signals: hidden means the page is not on screen, blurred means it is not receiving keystrokes. Be able to give the side-by-side window example where a page is visible but not focused.
Explain the divergence cases precisely and pick the right signal per concern — expensive work and state saving on the visibility axis, input and attention behaviour on the focus axis — and mention document.hasFocus() as the synchronous read.
Show the failure mode you have shipped or fixed: pausing on blur freezes a page the user is still watching. Argue for recomputing a three-state mode rather than toggling flags from two unordered event streams.
Own the policy question: define application-wide what "active", "visible but idle" and "suspended" mean in terms of poll rates, animation and connections, so cost and freshness are a deliberate decision rather than a per-feature accident.
## Two independent axes A browsing context has two properties that beginners routinely merge into one idea: - **Visibility** — is any of this document being presented to the user? Reported by `document.visibilityState` (`"visible"` / `"hidden"`), changes announced by the `visibilitychange` event on `document`. - **Focus** — is this window the one receiving keyboard input? Reported by `document.hasFocus()`, changes announced by the `focus` and `blur` events on `window`. They are independent because the operating system's window model is independent of the browser's tab model. A window can be on screen without being the active window, which is precisely the visible-but-blurred case. ## Concrete cases where they diverge | Situation | visibilityState | focused | |---|---|---| | Foreground tab, user typing in it | visible | yes | | Two windows side by side, user typing in the other app | visible | no | | User opens DevTools in a separate window and clicks in it | visible | no | | User switches to a different tab | hidden | no | | Window minimised, or phone screen locked | hidden | no | The second and third rows are the whole point of the question. The page is on screen, its animations are still being composited, its video is still playing to a watching user — but it is not the keyboard target. ## Why this matters in practice Suppose you pause a video, stop a canvas render loop, or halt data polling on `blur`. Now a user drags a chat window next to your dashboard and clicks it. Your dashboard is right there in front of them and it freezes. That is a real bug, and it is the classic consequence of using the focus signal where the visibility signal belonged. The inverse error is subtler: using visibility for input concerns. A typing-practice app that keeps its timer running while the user has clicked away into another window is measuring nothing useful, even though the page is visible. So the split is: - **CPU, battery, network, and state persistence → visibility.** Nobody can see a hidden page, and a hidden page may be frozen or killed without further notice, so hidden is both the "stop burning resources" cue and the "flush anything unsaved" cue. - **Input, presence, and attention → focus.** Caret blinking, keyboard shortcut activation, "are you still there" idle detection, marking a chat as read. ## Reading the state, not just the events Both axes have a synchronous reader, and you usually want both at startup rather than waiting for a transition: ```js function currentMode() { if (document.visibilityState === 'hidden') return 'suspended'; return document.hasFocus() ? 'active' : 'visible-idle'; } document.addEventListener('visibilitychange', update); window.addEventListener('focus', update); window.addEventListener('blur', update); update(); ``` That three-state model — suspended, visible but idle, active — is often the honest one for a dashboard: full rate when active, reduced rate when visible but idle, nothing at all when hidden. ## Ordering and pairing When the user switches tabs, both signals arrive: the window blurs and the document becomes hidden. Do not write code that depends on a precise ordering between the two — treat each handler as independently idempotent, recomputing the desired state rather than toggling a flag. Do not assume symmetry either: a hidden page may never become visible again, so cleanup that must happen belongs on the hidden path. A related trap is `blur` on a `window` versus `blur` on elements. Element `blur`/`focus` events do not bubble (their bubbling counterparts are `focusout` and `focusin`), so a handler attached high in the tree for element focus changes needs `focusin`/`focusout`, not `focus`/`blur`. Mixing the window-level signal up with element-level focus handling produces handlers that fire far more often than intended. ## The interview-ready summary "Visibility is about whether the pixels reach the user; focus is about where the keystrokes go. They are orthogonal — a side-by-side window is visible and blurred. I suspend expensive work and persist state on hidden, and I only use blur for input-shaped behaviour."
- Which API tells you synchronously whether the page currently has focus?`document.hasFocus()` returns a boolean for the focus axis, the way `document.visibilityState` reads the visibility axis. Both are worth checking once at startup, because a page can be loaded already hidden or already unfocused and no transition event has fired yet to tell your code about it.
- A colleague pauses a canvas render loop on window blur. What breaks?Anyone running the page beside another window sees it freeze while looking straight at it — clicking into a chat app, a terminal, or DevTools stops the animation. The correct trigger is the hidden visibility state; `blur` should at most reduce work, never suspend rendering the user can still see.
- Why can't you attach a window blur handler to catch focus moving between form fields?Window-level `blur` fires when the whole browsing context loses focus, not when focus moves inside it. Element `focus` and `blur` also do not bubble, so a delegated handler needs `focusin` and `focusout` instead — those are the bubbling versions designed exactly for watching focus movement within a subtree.
saying these in an interview costs you the question
- Treats blur as equivalent to the tab being hidden
- Pauses visible rendering when the window loses focus
- Claims a visible page is always the focused one
- Assumes blur and visibilitychange always arrive together in a fixed order
- Uses window blur to detect focus moving between inputs