A dialog in CodX Editor was set to max-height: 92vh. On a laptop it looked right. On a phone its buttons were unreachable, and no amount of scrolling brought them back.

The cause is that vh does not mean what most people assume it means.

Three viewports, not one

A mobile browser has a toolbar that slides away as you scroll. That gives the page two possible heights, and CSS defines a unit for each:

  • Large viewport (lvh) — the toolbar is hidden.
  • Small viewport (svh) — the toolbar is showing.
  • Dynamic viewport (dvh) — whichever is true right now, updating as the toolbar moves.

vh is an alias for the large viewport. It always measures the screen as though the toolbar were gone — even while the toolbar is visibly on screen taking up space.

So 92vh can be taller than the space you actually have. On an iPhone the toolbar is roughly 60–110px depending on state. A dialog sized to 92% of the toolbar-free height simply extends past the bottom edge, and whatever sits there — usually the confirm button — cannot be reached.

Why it survives testing

Nobody catches this by resizing a desktop window, because a desktop browser has no disappearing toolbar: vh and dvh are identical there. It does not appear in device emulation either, which renders a viewport without real browser chrome.

It only shows up on a physical phone, on first load, before you have scrolled enough to collapse the toolbar. That is a narrow window to test in and an easy one to miss.

The fix, and the trap inside it

The change itself is trivial:

/* before */
max-height: 92vh;

/* after */
max-height: 92dvh;

The trap is scale. Searching the editor's stylesheet turned up 38 height declarations using vh, spread across dialogs, panels, preview frames and the console. Fixing the one that was reported would have left thirty-seven others waiting.

They were converted in one pass, restricted to height properties so unrelated values were left alone:

const decl = /((?:max-|min-)?height\s*:\s*[^;{}]*?)(?<![a-z-])(\d+(?:\.\d+)?)vh/;

Six more were hiding in inline style attributes in the HTML, which a stylesheet search never touches. Worth remembering: if a project has inline styles, a CSS-only sweep gives a false sense of completion.

Where vh is still correct

This is not a blanket replacement. dvh changes value as the toolbar moves, so anything animated or transitioned against it will visibly jump while the user scrolls. For a decorative full-height hero, that jump is worse than the original problem.

  • Use dvh for anything that must stay reachable: dialogs, drawers, menus, anything with a button in it.
  • Use svh when an element must fit in the worst case and never resize afterwards.
  • Keep vh for decorative full-height sections where a little overflow does not matter.

One more detail: dvh needs Safari 15.4 or newer. A project that still supports older browsers should declare both, letting the newer unit win where it is understood.

height: 100vh;  /* fallback */
height: 100dvh; /* overrides where supported */

What to take from this

  • An unreachable button is a sizing bug, not a scrolling bug. If content is cut off at the bottom on mobile, check the units before rewriting the layout.
  • Fix the class, not the instance. One reported dialog meant thirty-eight quiet ones.
  • Search the HTML too. Inline styles do not appear in a stylesheet sweep.