DEV Community

Но Ол
Но Ол

Posted on

Fix: mobile 100vh jumps (URL bar + keyboard) using VisualViewport

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

  1. Read window.visualViewport.height (visible area height)
  2. Store it in a CSS variable: --vvh
  3. 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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Modal that shouldn’t be cropped by the keyboard

.modal {
  max-height: var(--vvh, 100vh);
  overflow: auto;
}
Enter fullscreen mode Exit fullscreen mode

“App shell” layout (header + content + footer)

.appShell {
  min-height: var(--vvh, 100vh);
  display: grid;
  grid-template-rows: auto 1fr auto;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Notes & gotchas (quick)

  • SSR: don’t touch window during 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

Top comments (0)