The viewport units that lie
100vh is taller than the screen on a phone, because the browser chrome retracts. This is the bug behind a thousand unreachable buttons.
On a phone, the browser’s address bar and toolbar retract as you scroll and come back when you scroll up. So the visible area has two sizes: large when the chrome is hidden, small when it is showing. The vh unit resolves against the large one, always, because that value never changes and a changing unit would cause constant relayout.
The consequence: height: 100vh on a hero section makes it taller than what the user can actually see when they arrive, because on arrival the chrome is showing. Whatever you put at the bottom of that hero, the call to action, almost always, is under the toolbar. It is not just below the fold, it is behind a piece of browser furniture, and on some layouts it cannot be scrolled to either.
Four units now exist. lvh is the large viewport, the same as vh. svh is the small viewport, which is what is visible when the chrome is showing, the safe one. dvh is dynamic and tracks the current value as the chrome moves. And the dvi/dvb variants follow the writing mode, matching the logical properties from the last lesson.
The choice is a trade. svh is stable and safe, and leaves a gap at the bottom when the chrome retracts. dvh always fills exactly what is visible, and relayouts continuously as the chrome animates, which can produce visible jank on anything complex inside it. For a hero with a button in it, svh is usually right, a small gap is better than a moving layout. For a full-screen app shell where filling the space matters more, dvh.
The instrument below has a real retracting chrome. Pick vh, scroll, and try to tap the button, a counter records the taps that miss because the button is really clipped out of the scroll port. Then switch to svh and try again. It takes about fifteen seconds and it is a more convincing argument than this paragraph.
The hero that hides its own button
The phone: scroll the page inside it
Ship faster
A hero sized 100vh, which is 400px on this screen.
What you get
Deploys on push
Logs you can read
A bill you can predict
Content under the hero. When the hero is measured in dvh, all of this moves while the bars animate.
| Unit | 100 of it | Moves |
|---|---|---|
| 100vh | 400px | no |
| 100svh | 320px | no |
| 100lvh | 400px | no |
| 100dvh | 320px | yes, every frame |
the large viewport, the old unit, and on phones it is lvh under another name
Hero 400px, viewport 320px. The button ends 66px past the fold, clipped by the toolbar. Try to tap it: you cannot, because it is not painted.
What you wrote
.hero {
height: 100vh; /* 400px on this phone */
display: flex;
flex-direction: column;
}
.hero .cta { margin-top: auto }100vh is 400px, measured against the viewport with the bars hidden. With the bars showing there are only 320px on screen, so the Get started button is 66px past the bottom edge, behind the toolbar. Nothing on this screen tells the user it is there.
When you see this in someone else’s code, and you will, constantly, in generated code most of all, the fix is one character. Changing 100vh to 100svh is usually the entire repair, and it is worth grepping for on every project you inherit.
Worth reading
- Kevin Powell: The problems with viewport units ↗, Demonstrated on a real phone, which is more convincing than any simulation.
- web.dev: The large, small, and dynamic viewport units ↗, The reference, with the diagrams that make the three sizes obvious.
You should now be able to
- Explain why 100vh overflows on mobile
- Choose correctly between svh, lvh and dvh
- Recognise the symptom in a layout someone else built
Loading…