100dvh fixed the old 100vh address-bar problem, and a lot of us quietly started treating it as "the mobile viewport fix." It isn't. It fixes one of the things that make a mobile web app's layout jump around. The on-screen keyboard is a different problem, and on iOS Safari dvh doesn't react to it at all.

This post is a decision table: which tool handles which mobile layout problem, and where it's easy to reach for the wrong one.

Four problems that look like one

When a full-height mobile layout breaks, it's usually one of these:

  1. Browser chrome — the address bar and toolbar expand and collapse as you scroll.
  2. The soft keyboard — it slides up and covers the bottom part of the screen.
  3. Safe areas — the notch, the rounded corners, the home indicator.
  4. Overlays that must stay reachable — bottom sheets, sticky toolbars, "Send" buttons.

They all show up as "my bottom bar is in the wrong place," so it's tempting to throw one fix at all of them. They need different tools.

What svh, lvh and dvh actually track

The viewport units are about browser chrome only:

  • svh — height when the browser UI is fully expanded (the small viewport)
  • lvh — height when the browser UI is retracted (the large viewport)
  • dvh — whatever it is right now, updating as the UI expands and collapses

That's it. None of them are defined in terms of the keyboard.

On iOS Safari, opening the keyboard doesn't resize the layout viewport. The keyboard overlays the page and only the visual viewport shrinks. So 100dvh stays the same height while the bottom of your layout is now behind the keyboard.

Chrome on Android behaves the same way by default these days (the keyboard resizes the visual viewport, not the layout viewport). There you can opt into the old "resize everything" behavior with the interactive-widget key:

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

Safari doesn't support interactive-widget, so you can't rely on it for iOS.

The decision table

Problem Tool Notes
Address bar / toolbar changing height svh / dvh svh as a stable floor, dvh when the layout should follow the chrome
Soft keyboard covering content window.visualViewport The only cross-browser signal that includes the keyboard
Notch / home indicator env(safe-area-inset-*) Needs viewport-fit=cover in the viewport meta
Bottom sheet or toolbar that must sit above the keyboard visualViewport + a CSS variable Only for the components that need it
Keyboard geometry in Chromium VirtualKeyboard API env(keyboard-inset-height); not available in Safari

A sane CSS default

For the app shell, start with plain CSS and no JavaScript:

.app-shell { min-height: 100svh; /* stable floor */ min-height: 100dvh; /* follow browser chrome where supported */ padding-bottom: env(safe-area-inset-bottom); } 
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> 

A couple of choices worth making on purpose:

  • height vs min-height. For document-like screens (articles, settings), min-height lets content grow. For a viewport-bound app UI (an editor with fixed chrome, a chat screen), you usually want a strict height and an inner scroll container.
  • dvh everywhere has a cost. dvh changes while the browser UI animates during scroll, so every element sized with it relayouts during that animation. Use it for the shell, not for every card.

The keyboard: visualViewport, scoped

When something genuinely has to stay above the keyboard, measure the visual viewport and expose it to CSS:

const vv = window.visualViewport; function updateKeyboardInset() { if (!vv) return; // How much of the layout viewport is hidden below the visual viewport. const hidden = window.innerHeight - vv.height - vv.offsetTop; document.documentElement.style.setProperty( '--keyboard-inset', `${Math.max(0, hidden)}px` ); } vv?.addEventListener('resize', updateKeyboardInset); vv?.addEventListener('scroll', updateKeyboardInset); updateKeyboardInset(); 
.composer-toolbar { position: fixed; left: 0; right: 0; bottom: var(--keyboard-inset, 0px); } 

Treat the number as an approximation. It moves during the keyboard animation, pinch-zoom changes it too, and the exact values differ between a Safari tab and a home-screen web app. Keep this logic to the one or two components that really need it instead of driving the whole layout from it.

The case dvh and visualViewport both miss

There's one more iOS behavior worth knowing: with the keyboard open, Safari can scroll the document itself, and position: fixed elements move with it. Whether that happens often depends on something boring — whether your inner scroll container still has room to scroll. If it doesn't, the gesture falls through to the document.

I wrote up that one separately, with a one-line CSS fix: The iOS Safari keyboard scroll bug, fixed with one line of CSS.

A real-device test pass

Simulators are fine for a first look, but keyboard and safe-area behavior needs a real phone. A quick pass that catches most of these bugs:

  1. Load the screen, scroll so the browser UI collapses, then expands again.
  2. Focus the bottom-most input. Is it visible? Is the toolbar still reachable?
  3. With the keyboard open, try scrolling short content and long content.
  4. Rotate the device with the keyboard open.
  5. Repeat in a Safari tab and as a home-screen web app.
  6. Check the bottom edge against the home indicator.

Takeaway

dvh is the right tool for the address bar and nothing else. For the keyboard, reach for visualViewport, and only in the components that need it. For the notch, env(safe-area-inset-*). Most "mobile viewport" bugs get easier once you figure out which of the four problems you're actually looking at.

I deal with this while building PenPage, a free, local-first Markdown notes app that runs in the browser. Notes stay on your device. Disclosure: it's my project.

This article was drafted with AI assistance.