CORE JSC

International Technology Partnership

UI/UX

Fixing 100vh Layout Bugs on Mobile Safari Caused by the Dynamic Browser Chrome

A full-height hero or modal that measures out perfectly in Chrome DevTools' device emulator is cut off, or scrolls when it shouldn't, on an actual iPhone. The CSS is correct; 100vh just doesn't mean what it looks like it means on Mobile Safari, where the browser's own address bar keeps resizing the viewport underneath the page.

Core JSC Team·August 3, 2026
UI/UXCSSMobile SafariViewportResponsive Design

The Problem

A full-height section styled with height: 100vh looks correct in a desktop browser and in emulated device previews, but on an actual iPhone or Android device running Safari, the same section is either cut off at the bottom (part of it hidden behind the browser's UI) or leaves the page scrollable when it visually shouldn't be. Scrolling down slightly to make the address bar collapse can make the layout jump, since the element's actual rendered height changes even though its CSS never did.

Why It Happens

Mobile Safari's address bar and toolbar aren't part of a fixed "viewport"

On desktop, the browser chrome (tabs, address bar, bookmarks) sits outside the page entirely, so 100vh reliably means "the full height of the visible page area," a number that doesn't change while scrolling. On Mobile Safari, the address bar and bottom toolbar collapse and expand as the user scrolls, physically changing how much vertical space is available to the page — but 100vh as originally specified is a static value, calculated once against the largest possible viewport (chrome collapsed), not the currently visible one.

The "layout viewport" and the "visual viewport" disagree

Mobile Safari actually tracks two different viewport concepts: a layout viewport (used for CSS calculations like vh) and a visual viewport (what's actually visible on screen right now, exposed via window.visualViewport). Classic vh units are pinned to the layout viewport's static, worst-case size, which is why a page can be styled at "100% of the viewport" and still not match what a user is actually looking at at any given scroll position.

The old workarounds (JS viewport-height polyfills) don't handle every case

A common older workaround sets a CSS custom property from window.innerHeight via JavaScript on load and resize — but this misses the continuous collapse/expand transition as the user scrolls (rather than resizes), and adds a layout-thrashing JS dependency for something that's now a solved problem at the CSS level in modern browsers.

The Fix

1. Use the dvh (dynamic viewport height) unit as the primary fix

.full-height-hero {
  height: 100vh; /* fallback for older browsers */
  height: 100dvh; /* tracks the *actual* visible viewport, chrome included */
}

dvh is defined specifically to track the real, currently visible viewport as Mobile Safari's chrome collapses and expands, rather than a static worst-case number — this alone resolves the majority of "cut off" and "unwanted scroll" cases in current Safari and Chrome versions without any JavaScript.

2. Use svh/lvh deliberately when you want the smallest or largest extreme instead

.always-fits-with-chrome-visible {
  height: 100svh; /* smallest possible viewport (chrome expanded) */
}
.fills-screen-when-scrolled {
  height: 100lvh; /* largest possible viewport (chrome collapsed) */
}

For content that must never be hidden behind the browser toolbar even in the worst case (a call-to-action button, for example), svh is the safer explicit choice; dvh's continuous resizing can otherwise cause that content to visually shift as the user scrolls.

3. Keep a JS fallback only for the browsers that don't yet support dvh

function setViewportHeightVar() {
  document.documentElement.style.setProperty(
    "--vh100",
    `${window.innerHeight}px`
  );
}
setViewportHeightVar();
window.addEventListener("resize", setViewportHeightVar);

Use this only as a fallback behind a @supports not (height: 100dvh) check, since maintaining a resize-listener workaround for browsers that already handle this correctly at the CSS level is unnecessary complexity and its own source of bugs.

4. Avoid 100vh for anything where a small mismatch breaks usability

For modals, fixed bottom sheets, or anything where content overlapping the browser chrome would hide an actionable button, prefer min-height: 100dvh with normal document flow over a hard height: 100vh lock — letting content that's taller than the viewport actually scroll is safer than forcing an exact height that can be wrong by the height of a collapsing toolbar.

Why This Works

The root issue was never a CSS mistake — it was that vh was defined before mobile browsers had collapsible chrome, so it measures a viewport concept that doesn't match what's actually visible at any given scroll position on Mobile Safari. dvh/svh/lvh exist specifically to give three different, explicit answers to "which viewport size do you mean," so choosing the right one for each use case (dynamic-tracking for full-bleed backgrounds, smallest-guaranteed for must-see UI) resolves the mismatch at the layer where the ambiguity actually originates, instead of patching around it with a resize listener.

Conclusion

100vh layout bugs on Mobile Safari happen because the unit was designed before viewports could dynamically resize as browser chrome collapses — it measures a static worst-case size, not what's actually visible right now. Replace it with dvh for content that should track the real visible viewport, use svh for content that must never be hidden behind browser UI, and reserve a JavaScript fallback only for browsers that don't yet support the newer units.