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:
- Browser chrome — the address bar and toolbar expand and collapse as you scroll.
- The soft keyboard — it slides up and covers the bottom part of the screen.
- Safe areas — the notch, the rounded corners, the home indicator.
- 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:
-
heightvsmin-height. For document-like screens (articles, settings),min-heightlets content grow. For a viewport-bound app UI (an editor with fixed chrome, a chat screen), you usually want a strictheightand an inner scroll container. -
dvheverywhere has a cost.dvhchanges 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:
- Load the screen, scroll so the browser UI collapses, then expands again.
- Focus the bottom-most input. Is it visible? Is the toolbar still reachable?
- With the keyboard open, try scrolling short content and long content.
- Rotate the device with the keyboard open.
- Repeat in a Safari tab and as a home-screen web app.
- 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.
