100vh Is a Lie on Mobile, and Three New CSS Units Fix It
If you have ever shipped a full-height hero, a modal, or a one-screen app on a phone, you have hit the 100vh wall. The element looks fine on desktop. On a phone, the bottom of your content sits behind the address bar, or the page jumps when the user scrolls and the bar collapses. The cause is simple: 100vh measures the viewport at its largest possible size, which on mobile is the size with the address bar hidden. Your user is looking at a smaller viewport than the one you told the browser to fill.
Browser vendors shipped a fix in 2022 and it has been stable for four years. Three new units, svh, lvh, and dvh, let you pick which viewport size you actually mean. This post covers what each one does, the fallback pattern for older browsers, and the gotchas that still trip people up.
Why 100vh breaks on mobile
On a phone in portrait mode, the browser window has two states. Address bar visible: the usable area is maybe 660 pixels tall on a typical device. Address bar hidden, after the user scrolls: the usable area is the full 800 pixels. When you write min-height: 100vh, the browser calculates 100 percent of the large viewport. Your hero is 800 pixels tall, but the user can only see 660 of them. The rest is trapped behind the address bar until they scroll, at which point the bar disappears and the hero snaps to fit, causing a visible layout shift.
The problem is worse for modals and full-screen overlays. A dialog sized to 100vh with the address bar visible is taller than the screen. The close button is off-screen, and the user cannot dismiss the dialog without scrolling, which defeats the purpose of a modal.
Three units that tell the truth
The dynamic units give you three explicit choices for what "one viewport height" means:
svh, small viewport height: 1 percent of the viewport when the address bar is visible. This is the smallest the viewport will ever be. Use it when you need to guarantee content fits the visible area without scrolling.lvh, large viewport height: 1 percent of the viewport when the address bar is hidden. This is the largest the viewport will ever be. Use it for backgrounds or decorative layers that should cover the maximum possible area.dvh, dynamic viewport height: 1 percent of the viewport right now, updating live as the address bar shows and hides. Use it for layouts that should track the actual visible area in real time.
The fallback pattern
Support landed in Chrome 108, Firefox 101, and Safari 15.4, all shipping in late 2022. By 2026 every mainstream browser supports the units and they are Baseline Widely Available. The standard pattern is a cascade, most conservative value first:
.hero {
min-height: 100vh; /* fallback: older browsers get the large viewport */
min-height: 100svh; /* small viewport: content always fits visible area */
min-height: 100dvh; /* dynamic: tracks the live viewport */
}The first line is the fallback. Browsers that do not recognise svh or dvh ignore those lines and use 100vh. Browsers that do recognise them apply the last valid declaration. You do not need @supports; the cascade does the work. Which of the last two lines you keep depends on the element. For a hero where you want the content fully visible without scrolling, stop at 100svh. For a full-page app layout that should track the viewport live, use 100dvh.
Which unit to use when
| Use case | Unit | Why |
|---|---|---|
| Modal or dialog that must fit the visible area | svh |
Guarantees the dialog is no taller than the smallest viewport, so the close button is always reachable. |
| Full-height hero or landing section | dvh |
Tracks the live viewport so the hero fills exactly what the user can see, updating as the bar animates. |
| Background or decorative layer | lvh |
Covers the maximum possible area so there is never a gap when the bar hides. |
| Sticky footer on a short page | svh |
Uses the smallest viewport so the footer does not overlap content when the bar is visible. |
Gotchas that still bite
Prefer min-height over height. If you set height: 100dvh, the element resizes every time the address bar animates. On a content-heavy page that triggers reflow and can cause jank during the scroll gesture. min-height: 100dvh lets the element grow beyond the viewport if the content is taller, avoiding the reflow loop.
dvh updates are not free. The browser recalculates the unit on every address bar state change. For a simple hero the cost is negligible, but for a page with hundreds of dvh elements you will see layout thrash during the scroll. Switch to svh, a static value, and accept that the element is slightly shorter when the bar is hidden.
Do not mix dvh and vh in the same property. Writing min-height: calc(100vh - 50px) alongside min-height: 100dvh is redundant. Pick one base unit and subtract your chrome offset from it. Mixing the two creates two competing calculations and makes debugging harder.
Where to apply it first
Search your stylesheet for 100vh and replace each occurrence with the cascade above. The most common offenders are hero sections, onboarding screens, and full-screen modals. In a typical marketing site, three or four rules cover most of the problem.
Then audit your component library. Any component using height: 100vh for a container should switch to svh if it is a dialog or dvh if it is a page section. This is a find-and-replace task with a few judgment calls, not a refactor.
The units are so small and so well-supported that the only reason not to use them is inertia. The 100vh fallback is still in your codebase because it worked on desktop and nobody remembered to check the phone. Fix it this week: your users stop seeing content trapped behind the address bar, and your layout shift scores improve without any JavaScript.
Comments