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
dvhfor anything that must stay reachable: dialogs, drawers, menus, anything with a button in it. - Use
svhwhen an element must fit in the worst case and never resize afterwards. - Keep
vhfor 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.