JavaScript Visual Viewport Table

PieceWhat it doesField note
window.visualViewportThe zoomed viewportWhat the user actually SEES - smaller than layout viewport when zoomed
vv.onresize / onscrollPinch and pan eventsZoom and keyboard changes fire here - not window.resize
vv.scaleThe pinch factor1 = no zoom; sticky headers un-stick when scale > 1
vv.offsetTop / offsetLeftWhere the view sitsThe visible slice of the layout viewport - pan offset
keyboard overlapvv shrinks, layout doesn'tOn mobile, the keyboard overlays - vv.height reports what remains
position: fixed breaksThe zoomed liefixed pins to LAYOUT viewport - drifted under pinch; anchor to vv
interactive-widget=resizes-contentThe meta switchOpt into layout-resize on keyboard - vv vs layout viewport converge
Scale UI by scaleThe zoom-aware fixSticky bars: translate by vv offsets - zoom-stable floating UI
Reference: the MDN Visual Viewport reference. window.visualViewport reports what the user ACTUALLY sees: when they pinch-zoom, the layout viewport stays put but the visual viewport shrinks and pans - vv.scale, vv.offsetTop and vv.height describe the visible slice. The classic bug it explains: position: fixed elements DRIFT under pinch zoom (fixed pins to the layout viewport, not the zoomed view). Bottom line: listen on vv.onresize/onscroll (not window.resize) for zoom and keyboard changes, anchor floating UI by translating with vv offsets for zoom-stable sticky bars, and the keyboard on mobile shrinks vv before it shrinks layout. Related tools: matchMedia table (the breakpoint-level view of the same screen), logical properties table (flow-relative layout that zooms gracefully), and observer table (the general change-watch family).

window.visualViewport reports what the user ACTUALLY sees: when they pinch-zoom, the layout viewport stays put but the visual viewport shrinks and pans - vv.scale, vv.offsetTop and vv.height describe the visible slice of the page.

Bottom line: the classic bug it explains is position: fixed DRIFT - fixed elements pin to the layout viewport, so under pinch zoom they slide away from the content they were stuck to. Zoom-aware floating UI anchors by translating with the visual viewport's offsets instead.

The honest part: on mobile the KEYBOARD also shrinks the visual viewport before it shrinks the layout one - vv.height reports what remains visible, which is why chat inputs built on vv behave and fixed-bottom inputs hide behind the keyboard.

How to use

  1. Listen on the right object: visualViewport.addEventListener('resize' and 'scroll') - pinch zoom and keyboard changes fire here, not on window.resize.
  2. Anchor floating UI to the zoom: translate the fixed bar by vv.offsetTop / vv.scale - it stays glued to the content instead of drifting.
  3. Read the keyboard: vv.height shrinking without a window resize means the keyboard is up - move the input above vv.height and it stays visible.

Frequently asked questions

What is the difference between the layout viewport and the visual viewport?

Two rectangles, one page. The LAYOUT viewport is what CSS layout resolves against - position: fixed pins to it, media queries measure it, and it does not change when the user pinch-zooms. The VISUAL viewport is the window onto the page the user currently sees - it shrinks when they zoom in (same CSS pixels, magnified) and pans as they drag. On desktop with no zoom they coincide; on mobile they diverge constantly. Every 'fixed element drifted when I zoomed' bug is the mismatch made visible: fixed is defined against layout, the user is looking at visual.

Why do my pinch-zoom and keyboard changes not fire window.resize?

Wrong event source. window.resize fires when the LAYOUT viewport changes - rotation, window drag, real browser resize. Pinch zoom and the mobile keyboard move the VISUAL viewport only: layout is untouched, so window.resize stays silent while the visible world changed. The visualViewport object fires its own resize and scroll events for exactly these - and on mobile keyboards, vv.height shrinking is the signal that the keyboard appeared (before any layout change, per browser). Code that adjusts floating UI must subscribe to visualViewport, not window - the events it needs live there.

How does position: fixed drift under pinch zoom?

Fixed pins to the layout viewport; zoom changes the visual one. When the user zooms to 200% on a corner of the page, the fixed element remains at its layout-viewport coordinates - visually it slides out of view or sits over unrelated content, because 'fixed to the screen' is only true at scale 1. The zoom-aware fix: keep the element fixed but translate it by the visual viewport's offset each frame - translate(vv.offsetLeft / vv.scale, vv.offsetTop / vv.scale) - and compensate sizes by the scale, so the bar tracks the user's actual view. This is the machinery behind zoom-stable sticky bars; without it, every fixed UI lies under zoom.

What does the interactive-widget meta switch change on mobile?

Whether the KEYBOARD resizes the layout viewport or just overlays it. By default on many mobile browsers the keyboard OVERLAYS the page: layout is untouched, only the visual viewport shrinks - which is why fixed-bottom inputs vanish behind the keyboard. interactive-widget=resizes-content opts the page into the older behavior: the keyboard shrinks the LAYOUT viewport, reflowing the page so bottom-anchored inputs land above the keyboard. The trade: resizes-content reflows the whole page on every keyboard toggle (jank on complex pages), while the overlay default keeps layout stable but pushes responsibility onto visualViewport-aware positioning - the two strategies this API exists to navigate.

Related tools