Stop Writing JavaScript To Fix 100vh On Mobile
Your full-screen hero looks perfect in the simulator and cuts off the CTA on a real phone. It's not a bug in your code — it's `vh` answering a question you didn't ask. Three new units fix it with zero JavaScript.

Your hero section is height: 100vh. Big background image, headline centered, a CTA button pinned near the bottom. You check it in the browser, check it in the iOS simulator, ship it.
Then the support tickets come in: "the sign-up button is cut off." You open it on your own phone to confirm — and it looks fine. Scroll down an inch, though, and there's a sliver of button peeking out from under where the fold should be.
Guess what's different between your simulator and a real phone in someone's hand — before you scroll.
The instinct is to blame the button, not the unit. Add padding, shrink the image, tweak breakpoints. None of it holds, because the box itself is the wrong size — 100vh is taller than what the user can actually see, and no amount of internal spacing fixes a container that's bigger than its own screen.
So the real fix people land on, for years now, has been JavaScript:
This works. It's also been copy-pasted into thousands of codebases as a shrug-emoji workaround — a resize listener recalculating a custom property, just so CSS can know something the browser already knows. It renders with the wrong height for one frame before the script runs. It has to be re-wired for every new full-screen element. And on some Android keyboards, "resize" fires in ways that make the hero jump while someone's mid-scroll.
You're not wrong to reach for it. vh really doesn't tell you what you think it tells you.
Here's the part that makes the bug feel random instead of consistent: 100vh isn't measuring "the screen right now." It's measuring the browser's largest possible viewport — the height you'd get if the address bar and toolbar were fully collapsed.
On iOS Safari, the address bar collapses when you scroll down and reappears when you scroll up or land on a fresh page. 100vh was sized for the collapsed state the whole time — so the moment that bar is visible (which, on a page load, it always is), your "full screen" hero is taller than the actual visible area by exactly the height of that bar. The bottom of your hero, CTA included, sits behind it.
And for years, Chrome on Android handled the same unit differently — sizing vh around the smallest viewport instead, bar included. Two major browsers, one CSS unit, two different answers to "how tall is the screen." That's why this bug never had a clean repro: it depended on the browser, the scroll position, and whether the user had just loaded the page or scrolled once already.
vh isn't lying. It's answering a question — "how tall is this viewport at its largest?" — that nobody actually asked.
Since 2022, every major browser engine ships three more precise viewport units, and they turn that whole JavaScript workaround into one CSS declaration:
svh— small viewport height. The height when browser UI (address bar, toolbar) is fully expanded. This is the safe, stable floor: content sized tosvhalways fits, bars showing or not.lvh— large viewport height. The height when browser UI is fully collapsed. This is whatvhwas already measuring on iOS Safari.dvh— dynamic viewport height. Tracks the real, current visible height, live, recalculating as the address bar shows and hides.
Swap the unit, delete the JavaScript:
Browsers that don't recognize dvh treat that whole declaration as invalid and ignore it — so the 100vh line above it quietly stands as the fallback, no @supports block required. Everywhere dvh is understood, the second line wins and the hero tracks the real viewport, live, as the bar animates in and out. No resize listener, no custom property, no one-frame flash of the wrong size.
The same trio exists for width (svw/lvw/dvw) and for logical dimensions (svi/lvi/dvi, svb/lvb/dvb, for writing-mode-aware layouts) — same three questions, different axis.
Try it before you keep reading: the playground below simulates a phone's address bar collapsing as you scroll, with three stacked panels — one sized in vh, one in svh, one in dvh — so you can watch all three respond (or fail to respond) to the exact same scroll.
Runs right in your browser — poke at it and watch the concept react live.
Reaching for dvh everywhere trades one bug for a subtler one. Because dvh recalculates while the address bar is mid-animation, anything sized with it visibly resizes as the user scrolls — a hero that gently grows and shrinks by fifty-odd pixels as the bar slides. For a full-bleed section that's supposed to feel locked to the screen, that's the right behavior. For, say, a sticky modal or a fixed bottom sheet, that same live resize reads as jitter.
A rough rule that holds up in practice:
dvhfor full-bleed sections you want to genuinely hug the visible viewport — landing page heroes, full-screen intro panels.svhfor anything that must never get cut off, even worst-case — a modal's max-height, a "fits on one screen" onboarding step. It's the pessimistic, always-safe number.lvhrarely, when you specifically want the old100vhbehavior on purpose (matching a background that's meant to bleed past the visible edge).
Support is solid enough to reach for today without a polyfill: Safari since 15.4 (March 2022), Chrome and Edge since 108 (December 2022), Firefox since 101 (May 2022). The height: 100vh; height: 100dvh; stacking pattern above is your fallback for anything older — no feature query needed, because the invalid-property-drop behavior does the work for you.
100vh was never broken — it was always precisely, consistently answering "how tall is this viewport with the browser chrome fully out of the way," which is a real answer to a question almost nobody meant to ask. dvh, svh, and lvh let you ask the actual question directly, in CSS, and delete the resize listener that's been standing in for them.
If you've got a --vh custom property hack sitting in a codebase right now — how old is it, and what would it take to rip out?
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.