dvh, svh and lvh: Choosing the Right Mobile Viewport Unit

On a desktop browser, 100vh is the height of the window and nothing more needs to be said. On a phone, the browser's address bar and bottom toolbar slide away as you scroll and slide back when you scroll up, so "the height of the viewport" is not one number. For years 100vh silently picked the larger one, and full-height heroes, login screens and modal sheets were cut off at the bottom when the page first loaded. The small, large and dynamic viewport units give each of those numbers a name. This page explains what each one measures, how they behave while scrolling, and which to reach for in each layout situation. It is part of Viewport Units & Mobile Layout in the Mastering Container Queries & Responsive Layouts guide.

Why one height was never enough

Mobile browsers hide their UI on scroll to give content more room. That design means the visible area changes size during ordinary use, and a layout unit that tracked it exactly would resize elements every time the user scrolled — expensive, and visually jarring. Early mobile browsers chose stability: they defined vh against the largest possible viewport, the one with the UI hidden, and kept it constant.

That choice made 100vh stable but wrong on first paint. The page loads with toolbars visible, so a 100vh element is taller than what the user can see, and whatever sits at its bottom edge — a call-to-action button, a sticky footer, the last field of a form — starts off screen. The fix could not be to redefine vh, because a decade of layouts depended on it being stable. Instead, the CSS Values specification added explicit units for each interpretation and left vh alone.

What each unit measures

Small, large and dynamic viewports Left phone with toolbars shown: the visible area is the small viewport and dynamic equals small. Right phone with toolbars hidden: the visible area is the large viewport and dynamic equals large. Two states, three units address bar 100svh = 100dvh toolbar UI shown (page load) 100lvh = 100dvh = 100vh UI hidden (scrolled)
  • svh — small viewport height. 1% of the viewport with every retractable UI element shown. It is the guaranteed-visible height: content sized to 100svh is never obscured by browser chrome.
  • lvh — large viewport height. 1% of the viewport with all retractable UI hidden. It is the maximum height the page can ever get.
  • dvh — dynamic viewport height. 1% of the current viewport. It equals svh when the UI is shown, lvh when it is hidden, and passes through the values in between as the UI animates.
  • vh — the classic unit. On mobile browsers with retractable UI it matches lvh. It is kept for compatibility, not recommended for new full-height layouts.

Each unit has width counterparts (svw, lvw, dvw), logical counterparts (svi/svb and so on) and min/max variants (svmin, dvmax). The width units rarely differ in practice, because mobile browser UI retracts vertically.

The complete implementation

The page below uses each unit where it fits: a hero sized to the small viewport so its call to action is visible on load, a background that uses the large viewport so no gap appears when the toolbar hides, and a full-screen sheet that uses the dynamic viewport so it always fits exactly.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Viewport units in context</title>
<style>
  html, body { margin: 0; }
  body { font: 16px/1.6 system-ui, sans-serif; }

  /* HERO — svh. The call to action at the bottom must be visible on first
     paint, when the toolbars are showing. svh never exceeds the visible area,
     and it does not change during scroll, so the hero never jumps. */
  .hero {
    min-height: 100vh;          /* fallback for engines without svh */
    min-height: 100svh;
    display: grid;
    align-content: space-between;
    padding: 1.5rem;
    box-sizing: border-box;
  }
  .hero__cta {
    justify-self: start;
    padding: 0.75rem 1.25rem;
    border-radius: 8px;
    background: #1d4ed8;
    color: #ffffff;
    text-decoration: none;
  }

  /* FIXED BACKGROUND LAYER — lvh. As the toolbar retracts the visible area
     grows; a layer sized to lvh is already that big, so no strip of
     uncovered page appears at the bottom. */
  .backdrop {
    position: fixed;
    inset: 0 0 auto 0;
    height: 100lvh;
    z-index: -1;
    background: linear-gradient(#e0e7ff, #f8fafc);
  }

  /* SHEET — dvh. A full-screen overlay must match the visible area exactly
     at all times, or its bottom button hides behind the toolbar. dvh tracks
     the toolbar, which is precisely the requirement here. */
  .sheet {
    position: fixed;
    inset: 0;
    height: 100vh;
    height: 100dvh;
    display: grid;
    grid-template-rows: auto 1fr auto;
    background: #ffffff;
  }
  .sheet[hidden] { display: none; }
  .sheet__body { overflow: auto; padding: 1rem; }
  .sheet__footer { padding: 1rem; border-top: 1px solid #e2e8f0; }
</style>
</head>
<body>
  <div class="backdrop" aria-hidden="true"></div>
  <header class="hero">
    <h1>Plan your trip</h1>
    <a class="hero__cta" href="#start">Start planning</a>
  </header>
  <main id="start">
    <p>Page content continues here…</p>
  </main>
  <div class="sheet" hidden>
    <h2>Filters</h2>
    <div class="sheet__body">Long list of filters</div>
    <div class="sheet__footer"><button type="button">Apply</button></div>
  </div>
</body>
</html>

Every rule declares a vh fallback first. An engine that does not recognise svh or dvh treats that declaration as invalid and keeps the earlier one, so older browsers get the classic behaviour rather than no height at all.

The key technique: match the unit to what must never happen

The three units trade stability against accuracy, and the right choice falls out of asking what failure you cannot accept for a given element.

Pick the unit by the failure you must avoid Content hidden under the toolbar on load is prevented by svh. A gap appearing when the toolbar hides is prevented by lvh. A size mismatch at any moment is prevented by dvh, at the cost of resizing during scroll. What must never happen? Bottom content hidden behind the toolbar heroes, landing screens, onboarding svh A bare strip when the toolbar retracts fixed backdrops, parallax layers lvh Any mismatch with the visible area full-screen sheets, app shells dvh

The instinct after learning about dvh is to use it everywhere, because it is always "correct". Resist it for anything in the normal document flow. A hero sized to 100dvh grows by the toolbar's height the moment the user starts scrolling, which shifts everything below it downward in the middle of the gesture. Browsers mitigate the cost by updating dvh in steps rather than on every frame, but the layout shift is real and can show up in Cumulative Layout Shift measurements. svh gives the hero a height that is correct on load and never changes, which is what you want for content.

dvh belongs to elements that are out of flow and must match the screen exactly — overlays, bottom sheets, full-screen app shells where the page itself does not scroll. For those, resizing along with the toolbar is the point, and no other content moves as a result. The app-shell variant is developed in Sticky Footer With Flexbox or Grid, which uses dvh for a shell whose middle region scrolls.

The on-screen keyboard is a separate problem

None of the three units changes when the virtual keyboard opens, by default. Modern mobile browsers overlay the keyboard on top of the page and shrink only the visual viewport, leaving the layout viewport — the one these units measure — untouched. That is why a bottom-anchored "Send" button in a chat UI can end up hidden behind the keyboard even with 100dvh.

Keyboard overlay versus resized layout viewport Left, the keyboard covers the bottom of a 100dvh shell and hides its composer. Right, with interactive-widget set to resizes-content, the shell shrinks and the composer sits just above the keyboard. Where the composer ends up when the keyboard opens messages keyboard covers composer default: overlays messages composer keyboard resizes-content

The viewport meta tag's interactive-widget key changes that behaviour. interactive-widget=resizes-content asks the browser to shrink the layout viewport when the keyboard appears, which in turn shrinks dvh, so a 100dvh shell with a bottom composer keeps the composer just above the keyboard.

<meta name="viewport"
      content="width=device-width, initial-scale=1, interactive-widget=resizes-content">

Support for interactive-widget is uneven across mobile browsers, so treat it as an enhancement and test the specific flows — chat, search, checkout — on real devices. For layouts where the keyboard matters, it is also worth keeping the input near the top of the scroll area so the browser's own scroll-into-view behaviour can reveal it.

Variation: clamping a hero between sensible bounds

A hero that is exactly one viewport tall is a strong design on a phone and an oddity on a 27-inch monitor, where it becomes a vast empty band. Combining viewport units with clamp() bounds it in both directions while keeping the small-viewport guarantee on mobile.

.hero {
  /* At least 28rem so the content always fits, at most 60rem so large
     monitors do not get a screen-filling banner, and 100svh in between
     so phones get a genuine full-screen hero. */
  min-height: clamp(28rem, 100vh, 60rem);
  min-height: clamp(28rem, 100svh, 60rem);
}

/* Landscape phones are short: let the hero shrink to its content there. */
@media (orientation: landscape) and (max-height: 30rem) {
  .hero { min-height: auto; }
}

The landscape rule protects against a subtle accessibility problem: on a short landscape screen a 28rem minimum is taller than the viewport, so the call to action is off screen again. WCAG 1.4.10 Reflow applies to height as well as width in this sense — content should remain usable without two-dimensional scrolling — and a hero that insists on a large minimum height fights that. For heights driven by the component rather than the viewport, Viewport Units vs Container Units explains when cqb is the better reference.

Browser support

The small, large and dynamic viewport units (svh, lvh, dvh and their width, logical and min/max variants) are supported in Chrome and Edge 108+, Firefox 101+ and Safari 15.4+. clamp() is supported in Chrome and Edge 79+, Firefox 75+ and Safari 13.1+. The vh fallback declaration written first covers anything older. The interactive-widget viewport key is supported in Chromium-based mobile browsers; elsewhere it is ignored, which leaves the default overlay behaviour.

FAQ

What is the difference between svh, lvh and dvh?svh is 1% of the small viewport, measured with all browser toolbars shown. lvh is 1% of the large viewport, measured with toolbars retracted. dvh is 1% of whichever size is current, so it changes as the toolbars slide in and out.

Is vh the same as lvh? On mobile browsers with retractable toolbars, vh is defined to behave like the large viewport, so 100vh equals 100lvh and is taller than the visible area while the toolbar shows. On desktop there are no retractable toolbars and all four units agree.

Should I replace every vh with dvh? No. dvh changes while the user scrolls on mobile, so anything sized with it resizes during scrolling, which can cause visible jumps and extra layout work. Use svh for heroes and above-the-fold sections, dvh for full-screen overlays that must exactly fit, and lvh for backgrounds that should never show a gap.

Do the new units account for the on-screen keyboard? Not by default. Most mobile browsers overlay the keyboard on the page without resizing the viewport, so dvh does not shrink when it opens. The interactive-widget key in the viewport meta tag can opt into resizing the layout viewport, which then changes dvh.

Related articles

More pages in the same section.