Mobile browsers love making 100vh feel… optimistic.
You build a full-height layout, it looks fine — then the URL bar collapses, the on-screen keyboard opens, and suddenly:
- footers jump
- modals get cropped
- “full screen” sections aren’t full-screen anymore
Here’s a small, production-friendly approach: use the Visual Viewport (what the user can actually see) and expose it to CSS.
Why 100vh breaks on mobile
On desktop, 100vh usually matches the visible area. On mobile, the visible area changes frequently because of:
- browser UI (address bar/toolbars)
- dynamic UI on scroll
- the on-screen keyboard
So 100vh can behave like “maximum possible height” rather than “currently visible height”.
The idea
- Read
window.visualViewport.height(visible area height) - Store it in a CSS variable:
--vvh - Use
min-height: var(--vvh)for full-height containers
This tends to behave better when:
- the URL bar expands/collapses
- the keyboard opens/closes
- you’re inside certain in-app webviews
Step 1 — JS: write the visible height into --vvh
Add this once on the client (no deps):
function setVVH() {
const vv = window.visualViewport;
const h = vv?.height ?? window.innerHeight;
document.documentElement.style.setProperty("--vvh", `${Math.round(h)}px`);
}
setVVH();
window.visualViewport?.addEventListener("resize", setVVH);
window.visualViewport?.addEventListener("scroll", setVVH);
window.addEventListener("resize", setVVH);
Why Math.round()?
Some browsers report fractional heights, which can cause tiny “layout jitter”. Rounding makes it steadier.
Step 2 — CSS: use it for full-height layouts
.fullHeight {
min-height: var(--vvh, 100vh);
}
I prefer min-height because it’s more forgiving for real content (forms, error messages, dynamic blocks). If you truly need strict sizing, you can use height, just be aware it may feel more brittle.
Practical examples
Full-page container
<main class="fullHeight">
...
</main>
Modal that shouldn’t be cropped by the keyboard
.modal {
max-height: var(--vvh, 100vh);
overflow: auto;
}
“App shell” layout (header + content + footer)
.appShell {
min-height: var(--vvh, 100vh);
display: grid;
grid-template-rows: auto 1fr auto;
}
Optional: make updates smoother with requestAnimationFrame
If you notice frequent events, schedule updates:
let scheduled = false;
function setVVH() {
const vv = window.visualViewport;
const h = vv?.height ?? window.innerHeight;
document.documentElement.style.setProperty("--vvh", `${Math.round(h)}px`);
}
function scheduleVVH() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
setVVH();
});
}
setVVH();
window.visualViewport?.addEventListener("resize", scheduleVVH);
window.visualViewport?.addEventListener("scroll", scheduleVVH);
window.addEventListener("resize", scheduleVVH);
Notes & gotchas (quick)
-
SSR: don’t touch
windowduring server render. Run this on the client. - Zoom: VisualViewport reacts to zoom; most apps are fine, but it’s good to know.
-
Webviews: behavior varies, but this approach usually beats raw
100vh.
Links
- Code (repo): https://github.com/AntonVoronezh/viewport-truth
Top comments (0)