dvh (Dynamic Viewport Height) is a modern CSS unit that adapts to the currently visible height of the browser viewport. It sounds like a small detail, but in practice it fixes a bug almost every web developer has run into.

If you've ever built a full-screen hero section and watched the browser's address bar cut off the bottom of it on mobile — or seen a stray scrollbar appear on a page designed to fit exactly one screen — this one's for you.

This guide covers the whole picture: why 100vh broke, what svh, lvh and dvh actually mean, ready-made recipes for the most common layouts, and the pitfalls you genuinely hit on real projects.

Why 100vh caused problems

By definition, vh is one percent of the viewport height. On desktop that's unambiguous: the browser window's content area doesn't change as you scroll.

On mobile it does. The address bar and the bottom navigation controls collapse as you scroll down and reappear as you scroll up, so the visible area is constantly moving. Browsers had to decide what vh should mean in that situation, and most of them chose to lock it to the largest possible value — the one you get once the browser chrome has fully collapsed.

That had a few very visible consequences:

  • a height: 100vh section extends below the screen on first load,
  • a layout designed to be exactly one screen tall becomes scrollable anyway,
  • the button or scroll cue placed at the bottom of the hero slides out of view.

For years we worked around it: measure the real height in JavaScript, write it into a custom property (--vh), then multiply it back with calc(). It worked, but it solved a layout problem with runtime code.

The new family of viewport units

CSS no longer knows just one viewport height — it knows three, because on mobile there really are three sensible answers to the question "how big is the screen?"

  • svh (small viewport height) — the smallest visible height, i.e. when all the browser UI is showing.
  • lvh (large viewport height) — the largest visible height, once the address bar and navigation have collapsed. This is what most browsers report as vh.
  • dvh (dynamic viewport height) — the current value, which tracks the browser chrome as it moves.

The order is always the same: svhdvhlvh. So 100dvh isn't a fixed number — it's a live value moving between those two bounds, always equal to the space that's genuinely available right now.

vh tells you how big the screen can be. dvh tells you how big it is.

Worth knowing: on desktop and in an installed (standalone) PWA there is no collapsing browser chrome, so all three units resolve to the same value. The difference only shows up where something actually moves — that is, in a mobile browser.

Quick reference: which unit when?

If you take one thing away from this article, make it this table. The rest of the guide is mostly an expansion of it.

Unit What it resolves to Changes on scroll? Use it for
vh the legacy behaviour; on mobile usually the large viewport no in new code, at most as a fallback
svh the smallest visible height no anything that must always fit: app shells, bottom bars, scroll-snap
lvh the largest visible height no rarely: background layers where overflow is intentional
dvh the current visible height yes hero sections, full-screen overlays, modals

A good rule of thumb: dvh for design, svh for guarantees. If something must always be visible, don't leave it to the dynamic value.

The most common case: the hero section

The basic usage is exactly as simple as it looks:

.hero {
    min-height: 100dvh;
}

There are two small but important details in those three lines. First, min-height rather than height: if the content ever grows taller than one screen it flows on instead of being clipped. Second, dvh already gives the correct result on its own wherever it's supported.

If you still need to serve older browsers, the fallback is one extra line. CSS applies the last declaration it understands, so a browser that doesn't know dvh simply skips the second one:

.hero {
    min-height: 100vh;  /* older browsers */
    min-height: 100dvh; /* everyone else */
}

No @supports, no JavaScript, no build step required.

A hero below a fixed header

In practice a hero rarely starts at the very top of the screen — there's usually a fixed navigation bar above it. Viewport units work inside calc() like any other length, so subtracting the header height is straightforward:

:root {
    --header-h: 4rem;
}

.hero {
    min-height: calc(100vh - var(--header-h));
    min-height: calc(100dvh - var(--header-h));
    display: grid;
    place-content: center;
}

Keep the header height in a custom property: the same value can then drive scroll-margin-top, the mobile menu's position and the hero calculation.

Recipes for real layouts

With viewport units the question is rarely "what does dvh mean" — it's which unit is right in this specific layout. Four recurring cases, with the code.

1. App shell: header, scrollable body, bottom bar

The classic app-like interface: a header on top, a navigation bar at the bottom, scrollable content in between. This one wants svh, not dvh.

.app {
    height: 100svh;               /* never promises more room than exists */
    display: grid;
    grid-template-rows: auto 1fr auto;
}

.app__main {
    min-height: 0;                /* without this the grid row can't shrink */
    overflow-y: auto;
}

Two things are going on. First, min-height: 0: in grid and flex layouts an item won't shrink below its content by default, so without it the scrollable area pushes the bottom navigation off the screen. This is the single most common reason a layout that "should work" overflows anyway.

Second, svh: because the scrolling happens inside an element rather than in the document, the browser chrome typically never collapses at all. svh is the only value guaranteed to stay true here.

2. Full-screen sections with scroll snap

Slide-like, full-screen sections that snap into place. svh is the right choice here too, and for a principled reason:

.slides {
    height: 100svh;
    overflow-y: auto;
    scroll-snap-type: y mandatory;
}

.slides > section {
    height: 100svh;
    scroll-snap-align: start;
}

If the height of the snap points changed while scrolling, the browser would be working against its own calculations: every scroll would move the target it's trying to reach. The result is a jumpy, "snapping back" scroll. svh is a fixed value, so every slide is guaranteed to fit and snapping stays predictable.

3. Bottom sheet

Here dvh really shines: a fixed overlay's whole job is to match the area that is visible right now.

.sheet {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    max-height: 85dvh;            /* leaves a sliver of the page behind */
    overflow-y: auto;
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

max-height is the key: the sheet takes as much room as its content needs, but never grows past 85% of the screen. Short content stays short, long content becomes scrollable — with no extra rules.

4. Side drawer

And one recipe where the best answer is that there is no viewport unit in it:

.drawer {
    position: fixed;
    top: 0;
    bottom: 0;                    /* no height unit needed */
    inset-inline-start: 0;
    width: min(20rem, 80vw);
    overflow-y: auto;
}

top: 0; bottom: 0 achieves exactly what height: 100dvh would, except the browser doesn't have to re-resolve a moving unit to get there. More on that in the next section.

dvh or 100%? When you don't need a viewport unit

Viewport units are so convenient that it's easy to overuse them. Yet most "fill the available space" problems have nothing to do with the size of the screen.

Fixed overlays: inset: 0 beats 100dvh

For a full-screen modal backdrop, height: 100dvh works — but it needlessly ties the element to a moving value:

/* Works, but unnecessary */
.overlay {
    position: fixed;
    top: 0;
    inset-inline: 0;
    height: 100dvh;
}

/* Better */
.overlay {
    position: fixed;
    inset: 0;
}

The second version resolves against its containing block rather than a unit — nothing to compute, nothing to re-evaluate while scrolling, and no way for it to end up a pixel off.

Percentages relate to the parent, not the screen

height: 100% is a percentage of the parent element's height, and it only works when there's an unbroken chain of heights all the way up to html. That's why the well-known opening lines existed for years:

html, body {
    height: 100%;
}

Once you have that chain, 100% is often the more correct choice for nested elements: it fills its parent, not the screen. An image inside a card behaves properly even when the card is only half a screen tall.

Decision rule: reach for a viewport unit only when the element genuinely has to relate to the screen. If it relates to its parent, percentages, flex or grid are the right tools — and inside a component, the container query units (cqh, cqi).

The virtual keyboard

This is where most of the confusion lives, so it's worth being precise: the on-screen mobile keyboard does not change the value of dvh by default.

What the browser shrinks is the visual viewport — the part you can actually see — while the layout viewport that CSS units resolve against stays the same. Hence the classic bug: the bottom button of a 100dvh app shell disappears behind the keyboard the moment a user taps into a field.

The meta tag solution

The interactive-widget setting on the viewport meta tag tells the browser to shrink the layout viewport too. Dynamic viewport units then follow along:

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

There are three values:

  • resizes-visual — the default: only the visual viewport shrinks,
  • resizes-content — the layout viewport shrinks too, so dvh follows,
  • overlays-content — the keyboard simply floats on top and nothing changes.

This setting applies to the whole page, so it isn't something to turn on for the sake of one component — it makes sense when the page is form-like or chat-like by nature. Support varies between browsers, so treat it as progressive enhancement: the page must still work without it.

When you need exact numbers: the Visual Viewport API

Where something genuinely has to sit above the keyboard — a chat input, a mobile form toolbar — the Visual Viewport API gives you the precise value:

const vv = window.visualViewport;

if (vv) {
    const sync = () => {
        document.documentElement.style
            .setProperty('--vvh', `${vv.height}px`);
    };
    vv.addEventListener('resize', sync);
    vv.addEventListener('scroll', sync);
    sync();
}

Yes, this brings JavaScript back — but only for this one genuinely special case, not for every full-screen section on the site. That's the difference from the old --vh hack: there the rule needed JavaScript, here only the exception does.

Safe areas: env(safe-area-inset-*)

100dvh tells you how large the visible area is — not how much of it is actually usable. The notch, the rounded corners and the bottom gesture bar are all inside the visible area, and none of them are places you want to put content.

Two things are needed. First, opt into edge-to-edge rendering in the meta tag:

<meta name="viewport"
      content="width=device-width, initial-scale=1, viewport-fit=cover">

Then use the env() function to stay out of the unsafe zones:

.hero {
    min-height: 100dvh;
    padding-block-end: calc(2rem + env(safe-area-inset-bottom, 0px));
}

.bottom-bar {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

The second argument (0px) is the fallback: without it, an unknown environment variable would invalidate the whole declaration in browsers that don't recognise it. Always write it out.

Common misunderstanding: without viewport-fit=cover the safe-area-inset-* values are all zero. If you've added the env() calls and nothing changes, that's almost certainly what's missing.

What to watch out for

The strength of dvh is also its limitation: the value genuinely changes while you scroll, and the browser has to re-run layout because of it.

Don't animate it

If a height expressed in dvh has a CSS transition on it, every movement of the browser chrome kicks off an animation. The result is a jumpy, stuttering layout. Keep the size change instant here.

/* This will stutter on mobile */
.hero {
    min-height: 100dvh;
    transition: min-height 0.3s ease;
}

Mind the content shift

If a 100dvh block sits in the middle of a long page, its height — and with it the position of everything after it — moves as you scroll. Put full-screen sections near the top of the page, or use svh further down.

Keep it out of media queries

A breakpoint condition should be stable. If it depends on a value that moves while you scroll, the media query can flip back and forth and cause bugs that are near-impossible to reproduce afterwards. Media queries want svh or lvh; leave dvh to the layout.

Updates aren't necessarily frame-accurate

The specification allows browsers to update the dynamic viewport size less often than the chrome actually moves. That's deliberate: it avoids re-running the entire layout on every frame. In practice it means you can rely on dvh as layout, but shouldn't build anything on it that expects frame-accurate tracking. That's what the Visual Viewport API is for.

Inside an iframe, the iframe is the viewport

Within an iframe, viewport units resolve against the iframe's own size, not the embedding page's. For an embedded widget, 100dvh almost certainly doesn't mean what you expect — percentages or container query units are the right tools there.

Not just height

The same logic runs through all the viewport units, not only height. Each one gained an s, l and d prefix:

  • dvw — dynamic viewport width,
  • dvmin and dvmax — the smaller and the larger of the two,
  • dvi and dvb — inline and block size, independent of writing mode.

In practice dvh is the reason the family exists — width changes far less often mid-scroll on mobile. dvi and dvb earn their keep when a site serves multiple writing modes: in vertical writing the "block direction" is horizontal, and the logical units follow that automatically.

What dvw does not fix: the classic 100vw horizontal overflow on desktop. If a 100vw-wide element makes the page scroll sideways, dvw behaves exactly the same way. For that, the answer is still width: 100%.

Migrating off the JavaScript --vh hack

If your project is a few years old, chances are this code is still in there — it was the standard workaround for a long time:

// Old workaround — safe to delete
const setVh = () => {
    document.documentElement.style
        .setProperty('--vh', `${window.innerHeight * 0.01}px`);
};

setVh();
window.addEventListener('resize', setVh);
.hero {
    height: calc(var(--vh, 1vh) * 100);
}

The whole thing is replaced by this:

.hero {
    min-height: 100vh;
    min-height: 100dvh;
}

The swap, step by step:

  1. search the codebase for --vh, innerHeight and resize,
  2. replace calc(var(--vh) * 100) with 100dvh — or 100svh wherever guaranteed room is required,
  3. delete the JavaScript and the resize listener,
  4. review where height is still used somewhere min-height belongs,
  5. verify on a real device (see the next section).

A note on step 2: the old hack measured innerHeight at load time, while the browser chrome was still showing — so what it actually captured was the small viewport. If you want identical behaviour, the exact equivalent is svh. dvh is better because it keeps tracking, so pick svh only where something was deliberately built on the fixed height.

The swap saves more than lines of code: the native unit is already correct at first paint, whereas the JavaScript version always flashed a wrong height before it ran.

How to test it

This section matters, because most dvh-related bugs cannot be reproduced in a desktop browser — including DevTools' device emulation.

Mobile emulation doesn't collapse the address bar, because there isn't one. So svh, lvh and dvh all report the same value there, and the exact phenomenon you're trying to catch stays invisible. You need a real device: use chrome://inspect on Android and Safari's Web Inspector on iOS.

A ruler for all three units

The fastest way to actually see what the three units return on a given device is to paste in this temporary snippet.

// Temporary ruler — don't ship this
const probe = (unit) => {
    const d = document.createElement('div');
    d.style.cssText = `position:fixed;visibility:hidden;height:100${unit}`;
    document.body.append(d);
    return d;
};

const p = { svh: probe('svh'), lvh: probe('lvh'), dvh: probe('dvh') };

const out = document.createElement('div');
out.style.cssText =
    'position:fixed;z-index:9999;top:0;left:0;padding:.4rem;' +
    'background:#000;color:#0f0;font:12px monospace';
document.body.append(out);

(function tick() {
    out.textContent =
        `svh ${p.svh.offsetHeight} · ` +
        `lvh ${p.lvh.offsetHeight} · ` +
        `dvh ${p.dvh.offsetHeight}`;
    requestAnimationFrame(tick);
})();

Scroll down and back up: svh and lvh stay put while dvh travels between them. If all three read the same, you're either on desktop or in an installed PWA — there's nothing to track in either case. (offsetHeight rounds to whole pixels, so a one-pixel difference is normal.)

Testing checklist

  • iOS Safari: scroll all the way down, then back up — is anything clipped?
  • Android Chrome: the same, plus the area around the bottom gesture bar,
  • focus a text input — does the keyboard hide anything important?
  • rotate the device to landscape — screens are short in landscape, and a 100dvh hero easily becomes unusably cramped,
  • if the site is installable as a PWA, check it in standalone mode too,
  • turn on "reduce motion" if the section also animates.

Browser support

Dynamic viewport units have been supported by every major browser since Safari 15.4, Chrome 108, Edge 108 and Firefox 101. In 2026 that effectively covers the entire active user base, and the two-line vh fallback above takes care of the rest.

If you need more than that — for instance a layout that is structured differently in each case — @supports is available:

@supports (height: 100dvh) {
    .app {
        height: 100svh;
    }
}

In practice you rarely need it: the two-declaration fallback is shorter, more readable and achieves the same thing. @supports is justified when the old and new versions differ structurally.

If you've been carrying a JavaScript --vh workaround, you can safely delete it: the native unit is more accurate, faster, and doesn't flash before first paint.

Checklist

Before shipping a full-screen layout, run through this list:

  • min-height instead of height wherever content can grow,
  • a vh fallback line above the dvh one,
  • svh wherever things must be guaranteed to fit (app shell, bottom bar),
  • svh rather than dvh for scroll snapping,
  • inset: 0 instead of a height unit for fixed overlays,
  • no transition or animation on a size expressed in dvh,
  • no dvh inside media queries,
  • min-height: 0 on the scrollable track of a grid or flex layout,
  • safe-area-inset padding wherever viewport-fit=cover is used,
  • the keyboard case is thought through (interactive-widget or the Visual Viewport API),
  • the old JavaScript --vh hack has been deleted,
  • tested on a real mobile device, not just in an emulator.

Frequently asked questions

What is the difference between vh and dvh?

vh measures the viewport height, but on mobile browsers it doesn't follow the address bar and navigation controls as they move — most browsers lock it to the largest possible height. dvh adapts dynamically to the area that is actually visible, so it stays accurate while you scroll.

When should I use dvh?

It's ideal for full-screen hero sections, landing pages, mobile web applications, and any layout that should match the visible screen height. For fixed elements such as bottom bars or sticky CTAs, svh is often the safer choice, because it never promises more room than there really is.

Do modern browsers support dvh?

Yes. Modern versions of Chrome, Edge, Firefox and Safari all support the dvh unit. For older browsers a simple vh fallback is enough: declare the vh rule first, then the dvh one below it.

Why does my layout jump when I use dvh?

Because the value of dvh genuinely changes while you scroll, and every change triggers a new layout pass. If the element has a transition on its height, that movement becomes a visible animation. The fix is twofold: never animate a size expressed in dvh, and use svh instead for full-height blocks that sit in the middle of a long page.

Does dvh account for the mobile keyboard?

Not by default. The on-screen keyboard shrinks the visual viewport but leaves the layout viewport unchanged, so dvh keeps its value. If you want the keyboard to be taken into account, add interactive-widget=resizes-content to the viewport meta tag, or measure with the Visual Viewport API.

What is the difference between 100dvh and height: 100%?

100dvh always relates to the visible height of the screen, while height: 100% relates to the parent element and requires an unbroken chain of heights up to the html element. If an element should fill its parent, percentages are the right tool; reach for a viewport unit only when the element really has to match the screen.

Can I use dvh in a media query?

Better not to. A breakpoint condition should be stable: if it depends on a value that moves while you scroll, the media query can flip back and forth and produce bugs that are very hard to reproduce. Use svh or lvh in media queries and keep dvh for layout.

Should I replace every vh with dvh?

No. Replace vh where the element has to fill the visible screen — typically full-screen sections. Where vh is just a source of proportional sizing, such as a decorative element's height, the swap gains you nothing. And where you need guaranteed room, the right replacement is svh, not dvh.