Fix Mobile Full-Height Layouts with Tailwind v4 Dynamic Viewport Units

Fix the mobile 100vh bug with Tailwind dvh units. Use h-dvh, min-h-dvh and the svh/lvh variants so your full-height sections respect the browser toolbar.

Steven Richardson
Steven Richardson
· 7 min read

You build a full-height hero with h-screen, it looks perfect in the desktop devtools, then you open it on an iPhone and the bottom is shoved under the Safari toolbar. That gap has cost me more debugging hours than I want to admit, and for years the fix was a resize listener writing a CSS variable. Tailwind v4 ships the real fix as utilities: h-dvh, min-h-dvh, and the svh/lvh family.

Why 100vh breaks on mobile#

On desktop, 100vh equals the window height and nothing moves. On mobile it does not. Safari and Chrome measure vh against the largest possible viewport — the state where the address bar is collapsed and hidden. When the page first loads, the toolbar is visible, so the real visible area is shorter than 100vh. Your h-screen element is taller than the screen, and the bottom slides under the browser chrome.

That is why the old workaround existed:

// The hack we all copied for years
function setViewportHeight() {
  document.documentElement.style.setProperty(
    '--vh',
    `${window.innerHeight * 0.01}px`
  );
}
window.addEventListener('resize', setViewportHeight);
setViewportHeight();

It works, but you are shipping JavaScript to solve a layout problem, running a handler on every scroll-driven toolbar animation, and fighting a flash before the script runs. The CSS viewport units make all of that unnecessary.

dvh, svh, and lvh explained#

CSS added three viewport-height units that describe the toolbar states explicitly, and Tailwind exposes each as a height utility:

  • svh (small viewport height) — the smallest the viewport gets, measured with the toolbar fully visible. h-svh is height: 100svh.
  • lvh (large viewport height) — the largest the viewport gets, with the toolbar hidden. h-lvh behaves like the old 100vh.
  • dvh (dynamic viewport height) — follows the live viewport and recomputes as the toolbar shows and hides. h-dvh is height: 100dvh.

The whole matrix is available across height, min-height, and max-height:

<!-- height -->
<div class="h-svh"></div>
<div class="h-lvh"></div>
<div class="h-dvh"></div>

<!-- min-height and max-height variants exist too -->
<div class="min-h-dvh"></div>
<div class="max-h-svh"></div>

For most layouts dvh is what you want. It resizes smoothly as the address bar collapses on scroll, so the element always matches what the user can actually see. The one caveat: because dvh changes as you scroll, an element pinned to exactly 100dvh will grow and shrink, which can cause a subtle reflow. That is where picking svh or lvh deliberately matters, and I cover it below.

Full-height heroes with min-h-dvh#

The most common case is a landing hero that should fill the screen. Reach for min-h-dvh rather than h-dvh. The min-h variant guarantees the section is at least the viewport height, but lets it grow when the content is taller — so a hero with a long headline on a narrow phone still scrolls instead of clipping.

<section class="min-h-dvh flex flex-col items-center justify-center gap-6 px-6 text-center">
  <h1 class="text-4xl font-bold">Ship faster with less config</h1>
  <p class="max-w-prose text-lg text-gray-600">
    A full-height hero that fills the screen on mobile and never
    hides its call-to-action under the Safari toolbar.
  </p>
  <a href="#start" class="rounded-lg bg-black px-6 py-3 text-white">Get started</a>
</section>

Swap h-screen for min-h-dvh and the CTA stops disappearing behind the chrome. This pairs naturally with scroll-driven sections — if you are building a swipeable full-screen sequence, the same units feed straight into a scroll-snap carousel in Tailwind, where every panel needs to be exactly one viewport tall.

When to prefer svh or lvh#

dvh is the safe default, but it is not always the right call. The trade-off is stability versus fit.

Use svh when you need a layout that never overflows in any toolbar state. Because svh is the smallest height, a sticky footer or a fixed bottom action bar sized against svh stays fully on-screen even when the toolbar is showing. The cost is a bit of empty space once the toolbar collapses.

Use lvh when you want a stable, non-animating height and you are fine with content sitting under the toolbar on first paint — a background image that should bleed to the full extent, for example.

Avoid pinning interactive content to h-dvh if you dislike reflow: as the toolbar animates, a 100dvh element resizes on every frame, and anything vertically centred inside it will visibly shift. For a stable centred layout, min-h-svh is often calmer than h-dvh. This is the same "size to what the content actually needs" instinct behind Tailwind's field-sizing utilities for auto-growing textareas — let the browser resolve the real dimension instead of hard-coding it.

Browser support and fallback#

All three units — dvh, svh, lvh — reached Baseline "Widely available" in June 2025. They work in Chrome 108+, Edge 108+, Firefox 101+, and Safari 15.4+. In practice that is the overwhelming majority of mobile traffic, so for most projects you can use h-dvh directly.

If you support long-tail old devices, layer a vh fallback with Tailwind's supports-[…] variant. The base class applies everywhere; the variant overrides it only where the unit is understood:

<!-- h-screen (100vh) everywhere; h-dvh where the unit is supported -->
<section class="h-screen supports-[height:100dvh]:h-dvh">
  ...
</section>

If you prefer plain CSS in a component layer, the equivalent feature query reads the same way:

.hero {
  min-height: 100vh; /* fallback for old browsers */
}

@supports (min-height: 100dvh) {
  .hero {
    min-height: 100dvh;
  }
}

Since these units predate Tailwind v4, they are not a v4-only feature — but if you are landing on v4 through the automated v3-to-v4 upgrade tool, the height utilities come along for free and you can retire the JavaScript hack in the same pass.

Gotchas and Edge Cases#

A few things that have bitten me:

dvh measures the viewport, not a scroll container. If your app locks scrolling on the body and scrolls an inner element, sizing that inner element to h-dvh still tracks the browser viewport, which is usually what you want — but do not expect it to react to a resized parent.

Nested 100dvh elements stack. Two min-h-dvh sections make a page two viewports tall, which is correct but easy to forget when a wrapper and its child both carry the class.

On desktop, dvh, svh, and lvh all equal vh because there is no collapsing toolbar. Test on a real phone or a device emulator with the mobile toolbar simulated — Chrome's responsive mode does not always reproduce the collapse behaviour, so a passing desktop check proves nothing about the actual bug.

Combining dvh with 100% on a flex child can double up. If a parent is h-dvh and the child is h-full, that is fine; setting both to h-dvh is the trap.

Wrapping Up#

Delete the --vh JavaScript and the resize listener. Use min-h-dvh for heroes and full-height sections, reach for svh when a fixed bottom bar must stay visible, and add a supports-[height:100dvh]:h-dvh fallback only if your analytics still show meaningful pre-2022 browser traffic. From here, container queries for component-level responsive design are the natural next step for layouts that should respond to their container rather than the viewport.

FAQ#

Why is 100vh too tall on mobile?

Mobile browsers measure vh against the largest viewport state — the one where the address bar is collapsed and hidden. On page load the toolbar is visible, so the visible area is shorter than 100vh. An element set to h-screen ends up taller than the screen and its bottom is pushed under the browser chrome.

What is the difference between dvh, svh, and lvh?

svh is the small viewport height, measured with the toolbar visible, so it is the smallest value. lvh is the large viewport height with the toolbar hidden, matching the old 100vh behaviour. dvh is dynamic: it follows the live viewport and recomputes as the toolbar shows and hides.

How do I make a full-height section in Tailwind on mobile?

Use min-h-dvh instead of h-screen. In Tailwind that compiles to min-height: 100dvh, which fills the visible viewport and resizes as the toolbar animates, while still allowing the section to grow if the content is taller than the screen so nothing clips.

Is h-dvh supported in all browsers?

The dvh, svh, and lvh units reached Baseline "Widely available" in June 2025 and work in Chrome 108+, Edge 108+, Firefox 101+, and Safari 15.4+. That covers the vast majority of mobile users. For very old browsers, add a vh fallback using a @supports query or Tailwind's supports-[height:100dvh]:h-dvh variant.

Should I use min-h-dvh or h-dvh?

Prefer min-h-dvh for hero and content sections: it guarantees the element is at least a full viewport tall but lets it grow when the content is longer, so nothing gets clipped. Reserve h-dvh for cases where you truly need a fixed viewport-height box, and be aware it will resize as the toolbar animates, which can cause centred content to shift.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.