Dynamic Viewport Height Units: dvh, svh, and lvh
Introduction
height: 100vh has a famous mobile bug: a full-height hero section renders taller than the screen, with its bottom edge hidden behind the browser's own address bar. The cause is a quirk of how vh was defined long before mobile browsers routinely hid and showed their UI on scroll — 100vh has always meant the largest the viewport could possibly be, address bar retracted, even while the address bar is actually sitting on screen right now covering part of that height.
CSS now has three separate viewport-height units instead of one, precisely because "how tall is the viewport" turned out to have three different useful answers on mobile.
The Three Units
height: 100lvh; /* Large viewport — assumes browser UI is retracted (the OLD 100vh behavior) */
height: 100svh; /* Small viewport — assumes browser UI is fully showing */
height: 100dvh; /* Dynamic viewport — tracks whichever is ACTUALLY true right now, live */lvh and svh are both constants for a given device — they represent the two fixed endpoints (UI hidden, UI showing) and never change based on what the browser chrome is actually doing at this instant. dvh is the only one of the three that's genuinely dynamic: it re-evaluates as the address bar shows or hides while scrolling, which is also why it comes with a real trade-off — see Production Considerations below.
Try It Live
This demo mocks a phone's address bar with a toggle button, since there's no way to make a real mobile browser show/hide its UI inside an embedded preview. lvh and svh stay constant no matter what you click — only the dvh bar actually reacts.
Dynamic Viewport Height Units Playground
Toggle a simulated mobile address bar to see lvh and svh stay constant while dvh alone tracks the actual visible height live.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Dynamic Viewport Height Units Playground</title> </head> <body> <main class="playground"> <header class="playground-header"> <h1>Simulated Mobile Chrome</h1> <p>Toggle the address bar to see how <code>lvh</code>, <code>svh</code>, and <code>dvh</code> each answer "how tall is the viewport?" differently. This mocks a phone's browser chrome — a real device does this automatically as you scroll.</p> </header> <button class="toggle-btn" id="chrome-toggle" type="button">Hide address bar</button> <section class="demo-area" aria-label="Simulated phone viewport"> <div class="phone"> <div class="phone-screen" id="phone-screen"> <div class="unit-bar unit-lvh"> <span class="unit-name">lvh</span> <span class="unit-px" id="lvh-px"></span> </div> <div class="unit-bar unit-svh"> <span class="unit-name">svh</span> <span class="unit-px" id="svh-px"></span> </div> <div class="unit-bar unit-dvh" id="dvh-bar"> <span class="unit-name">dvh</span> <span class="unit-px" id="dvh-px"></span> </div> </div> <div class="phone-chrome" id="phone-chrome">address bar</div> </div> </section> <output id="readout" class="readout" aria-live="polite"></output> </main> <script src="./index.js"></script> </body> </html>
Starting sandbox…
No console output yet.
The Classic Bug, Fixed
/* Before: hero renders taller than what's actually visible until the address bar retracts */
.hero {
height: 100vh;
}
/* After: always matches the CURRENTLY visible height, address bar or not */
.hero {
height: 100dvh;
}This is the single most common reason to reach for dvh: any full-viewport section (a hero, a mobile modal, a fixed sidebar) that needs its actual, currently-visible height rather than an optimistic "if the address bar weren't there" height.
The Pre-dvh Fix: A JS-Computed --vh (and Why You Can Delete It)
Before dvh shipped, the same problem had a well-known JavaScript workaround: measure the actual visible height with window.innerHeight, store it in a custom property, and re-run that measurement on resize.
function setViewportHeightVar() {
// 1% of the ACTUAL visible height, remeasured every time the address bar shows or hides.
document.documentElement.style.setProperty("--vh", `${window.innerHeight * 0.01}px`);
}
setViewportHeightVar();
window.addEventListener("resize", setViewportHeightVar);.hero {
/* Multiply back up to 100 units, mirroring 100vh's own syntax. */
height: calc(var(--vh) * 100);
}This worked, but it meant every full-height element depended on a resize listener existing and firing before anyone noticed — miss that script, or run it after first paint, and the layout is briefly (or permanently) wrong. dvh is the browser doing this exact measurement natively: no listener to wire up, no custom property to multiply back out, and no flash of an incorrectly-sized layout while JavaScript catches up.
Why svh Still Has a Use
dvh sounds strictly better than svh — it's the "correct" one, so why would anything reach for a value that's pessimistically small? Because dvh recalculating on every scroll has a cost: layout that depends on it can visibly reflow while the address bar animates in or out, which is sometimes exactly the jank a fixed, non-reflowing layout wants to avoid:
.full-bleed-lockup {
/* A cover image/logo lockup that should NEVER shift height mid-scroll, even at the cost of a
small permanent gap once the address bar retracts. */
height: 100svh;
}The choice between dvh and svh is really "should this element be allowed to resize as the user scrolls" — dvh for anything that should always fill the true visible area, svh for anything that should stay perfectly still.
Where Should You Use This?
- Any full-height mobile section (
100vhhero, splash screen, fixed CTA bar) that currently gets cut off behind the address bar - Full-screen modals/sheets on mobile that need to match the actual visible viewport, not the optimistic maximum
- Layouts that must not reflow while scrolling — reach for
svhthere instead ofdvh - Any
100vhin the codebase today is worth an audit — it's very rarely the unit that was actually intended
Production Considerations & Edge Cases
dvhrecalculates on every scroll-driven UI change, which means anything sized or positioned from it can reflow mid-scroll. For content that must stay visually still, usesvh(orlvh) instead, even thoughdvhis the "more correct" number.- These units aren't limited to
height.dvw/svw/lvware the width equivalents, andvi/vbvariants exist for logical inline/block axes — the same large/small/dynamic distinction applies to all of them, though in practice the width units rarely diverge from each other the way the height units do; see Dynamic Viewport Width Units for why, and Viewport Inline & Block Units for the writing-mode-aware versions that don't commit to a physical direction at all. - Desktop browsers mostly don't show/hide chrome, so
lvh,svh, anddvhtypically resolve identically there — this is overwhelmingly a mobile-Safari/mobile-Chrome concern, not a desktop one. - Browser support is solid in every modern evergreen mobile browser (Chrome/Edge 108+, Firefox 101+, Safari 15.4+) as of this writing — safe to adopt for the mobile-chrome problem specifically; keep a plain
100vhfallback declared first for any browser old enough to not recognizedvhat all (an unsupported value is ignored, leaving the earlier declaration in effect). dvhcomposes withcalc(),env(), andclamp()like any other length. Two combinations come up constantly in production:height: calc(100dvh - env(safe-area-inset-bottom))keeps a fixed footer clear of a notched device's home-indicator area, andheight: clamp(20rem, 100dvh, 50rem)stops a full-height modal from stretching absurdly tall on a large tablet — see min(), max(), and clamp() for the function itself.
Key Takeaway
100vh was never wrong, exactly — it was just answering "how tall is the viewport at its largest," which mobile browsers made a poor default for "how tall is the screen right now." dvh answers the question most 100vh usages actually meant to ask; svh/lvh stay useful whenever a layout specifically needs one of the two fixed endpoints instead of the live value.
Both Holy Grail Layout and Responsive CSS Grid Layouts reach for min-height: 100vh to make a page fill the screen; swapping that for 100dvh is exactly the audit called out above, applied to a real layout.
Changelog
- — Initial publication.