JavaScript Visual Viewport Table
| Piece | What it does | Field note |
|---|---|---|
window.visualViewport | The zoomed viewport | What the user actually SEES - smaller than layout viewport when zoomed |
vv.onresize / onscroll | Pinch and pan events | Zoom and keyboard changes fire here - not window.resize |
vv.scale | The pinch factor | 1 = no zoom; sticky headers un-stick when scale > 1 |
vv.offsetTop / offsetLeft | Where the view sits | The visible slice of the layout viewport - pan offset |
keyboard overlap | vv shrinks, layout doesn't | On mobile, the keyboard overlays - vv.height reports what remains |
position: fixed breaks | The zoomed lie | fixed pins to LAYOUT viewport - drifted under pinch; anchor to vv |
interactive-widget=resizes-content | The meta switch | Opt into layout-resize on keyboard - vv vs layout viewport converge |
Scale UI by scale | The zoom-aware fix | Sticky bars: translate by vv offsets - zoom-stable floating UI |
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
- Listen on the right object: visualViewport.addEventListener('resize' and 'scroll') - pinch zoom and keyboard changes fire here, not on window.resize.
- Anchor floating UI to the zoom: translate the fixed bar by vv.offsetTop / vv.scale - it stays glued to the content instead of drifting.
- 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.