Learning on Web Dev Open is free for all.

Take It Apart > The mobile reality models forgetThe viewport units that lie
Phase 01The mobile reality models forget62 of 434

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.

Concept15 minAI pair

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

example.com

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.

Live valuesbars expanded
Unit100 of itMoves
100vh400pxno
100svh320pxno
100lvh400pxno
100dvh320pxyes, every frame

the large viewport, the old unit, and on phones it is lvh under another name

Get started is not reachabletaps landed: 0

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.

dvh and the jank it buys

A box measured in dvh is re-laid-out on every frame of the toolbar animation, up to 80px of height change spread over roughly 15 frames, and everything below it moves with it. Accumulated movement so far: 0px. Text reflows, anything sticky re-solves, and on a mid-range phone that is the difference between a smooth scroll and a stutter.

Use svh for anything that must be tappable without scrolling: a full-height landing panel, a drawer, a modal. It is 320px here rather than 400px, so it leaves a gap when the bars retract, and nothing ever ends up under a toolbar. Keep dvh for the cases where the size genuinely has to track, and keep it off anything holding text.

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.

Pick vh, scroll the phone, and try to press the button. The tap counter is the part that lands.

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

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
Ask the community

Loading…