Why 100vh can be tricky on mobile
Classic vh units were simple on desktop, but mobile browsers have dynamic address bars and toolbars. When browser chrome expands or collapses, a 100vh section can appear too tall, hide content, or create unexpected scrolling.
When to use 100dvh
The dvh unit reflects the dynamic viewport height. It is often a better choice for app-like panels, mobile sections, and full-screen layouts where the visible page height should respond to browser UI changes.
When to use svh and lvh
Small viewport height, or svh, assumes retractable browser toolbars are expanded; lvh assumes they are retracted. These values stay stable while toolbars animate, although resizing or rotating the viewport can change them. The default vh unit corresponds to lvh. An on-screen keyboard can overlay content without changing these units.
A safer CSS pattern
Use min-height so long content can grow and scroll. Use svh for a stable panel that fits with toolbars expanded, or dvh when resizing with those toolbars is intentional. Test keyboard focus separately: a reduced visualViewport.height does not prove that dvh changed or that a fixed submit button remains visible.
Safer full-height CSS example
Start with a simple fallback, then layer modern viewport units where supported.
.hero {
min-height: 100vh;
min-height: 100svh;
}
@supports (height: 100dvh) {
.hero {
min-height: 100dvh;
}
}
Reproduce toolbar and keyboard changes
Save the following as a separate HTML file, open it directly on a phone, scroll to retract the browser toolbar, then focus the input and rotate the phone. Compare the four measured CSS heights with the visual viewport. A desktop responsive preview alone cannot reproduce every mobile toolbar or keyboard policy.
<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Viewport-unit measurement</title>
<style>
body { min-height: 180vh; padding: 16px; }
.probe { position: absolute; width: 0; visibility: hidden; pointer-events: none; }
output { display: block; position: sticky; top: 0; background: white; white-space: pre-wrap; }
</style>
<output id="measurements" aria-live="polite"></output>
<label>Open keyboard <input type="text"></label>
<script>
const units = ['vh', 'svh', 'lvh', 'dvh'];
const probes = units.map(unit => {
const probe = document.createElement('div');
probe.className = 'probe';
probe.style.height = `100${unit}`;
document.body.append(probe);
return probe;
});
function measure() {
const rows = units.map((unit, i) => `${unit}: ${CSS.supports('height', `100${unit}`) ? probes[i].getBoundingClientRect().height.toFixed(1) : 'unsupported'}`);
rows.push(`visualViewport.height: ${window.visualViewport?.height ?? 'unavailable'}`);
document.querySelector('#measurements').textContent = rows.join('\n');
}
window.addEventListener('resize', measure);
window.addEventListener('scroll', measure, { passive: true });
window.visualViewport?.addEventListener('resize', measure);
measure();
</script>
Expected pattern with retractable toolbars: svh stays at the small reference, lvh and vh use the large reference, and dvh responds between those bounds. With an on-screen keyboard, the visual viewport may shrink while all four CSS measurements remain unchanged. Browser keyboard policies and resize timing differ; verify that a focused field and submit action remain reachable by scrolling.
Sources, checked September 5, 2026: W3C viewport-relative units and overlay keyboards; MDN visual viewport.