Layouts That Survive the Virtual Keyboard
A chat screen looks perfect in a mobile browser until someone taps the message field. The on-screen keyboard slides up, and depending on the browser the input bar disappears behind it, the header scrolls off the top, or the whole page shifts in a way the layout never expected. Desktop testing never reveals this, and even mobile emulators in DevTools do not show a keyboard. The behaviour comes down to which viewport the keyboard shrinks, and CSS now has tools to choose that behaviour and react to it: the interactive-widget viewport setting, dynamic viewport units, and keyboard environment variables. This page covers all three with fallbacks. It belongs to Viewport Units & Mobile Layout in the Mastering Container Queries & Responsive Layouts guide.
Two viewports and a keyboard
Mobile browsers track two viewports. The layout viewport is what CSS lays out against: position: fixed, viewport units and percentage heights on the root all refer to it. The visual viewport is the part the user can actually see, which shrinks when they pinch-zoom or when overlays such as the keyboard appear. When the keyboard opens, browsers choose one of three behaviours:
- resizes-visual — only the visual viewport shrinks. The layout stays the same size, the keyboard covers its bottom part, and the browser scrolls to keep the focused input visible. This is the common default.
- resizes-content — the layout viewport shrinks too. The page re-lays out into the space above the keyboard, and fixed-bottom elements sit on top of the keyboard.
- overlays-content — neither viewport shrinks. The keyboard covers the page and the author handles it, typically with the keyboard environment variables.
The complete implementation
A chat layout that uses a three-row grid sized to the dynamic viewport height, opts in to layout resizing where supported, and falls back gracefully everywhere else. The demo shows the layout itself; the keyboard behaviour can only be tested on a real device.
<meta name="viewport"
content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
html, body { margin: 0; block-size: 100%; }
.chat {
display: grid;
grid-template-rows: auto 1fr auto;
block-size: 100vh; /* fallback for browsers without dvh */
block-size: 100dvh; /* follows toolbars, and the keyboard when content resizes */
}
.chat__header { padding: 0.75rem 1rem; border-block-end: 1px solid #e2e8f0; }
.chat__messages {
overflow-y: auto;
overscroll-behavior: contain; /* do not scroll the page behind the thread */
padding: 1rem;
}
.chat__composer {
display: flex;
gap: 0.5rem;
padding: 0.75rem 1rem;
padding-block-end: max(0.75rem, env(safe-area-inset-bottom));
border-block-start: 1px solid #e2e8f0;
}
With interactive-widget=resizes-content, supporting browsers shrink the layout viewport when the keyboard opens. 100dvh follows, the grid re-lays out into the space above the keyboard, and the composer row sits directly on top of it — the same result a native messaging app gives. The message list scrolls inside its own row, so the header stays visible. In browsers that ignore the meta value, the layout still behaves sensibly: the browser pans the visual viewport to reveal the focused field, the composer comes into view with it, and closing the keyboard returns everything to where it was. The page never depends on the enhancement to remain usable, which is the property that matters most for a layout this central.
The key technique: size to the viewport, scroll inside
The layout works in every keyboard mode because it avoids position: fixed for the composer. Fixed elements are positioned against the layout viewport; in the common resizes-visual mode that viewport does not shrink, so a fixed composer ends up under the keyboard. A grid row, by contrast, is part of the document: the browser's own "scroll the focused input into view" behaviour brings it up above the keyboard even in resizes-visual mode, because the input is inside the composer.
Why 100vh is the wrong height here
On mobile, 100vh resolves to the large viewport height — the height with browser toolbars retracted. When the page first loads with the address bar visible, a 100vh layout is taller than the screen, and its last row, the composer, sits below the visible area before the keyboard is even involved. 100dvh tracks the toolbars as they show and hide, so the composer starts on screen. The implementation declares 100vh first as a fallback for browsers without dynamic units and overrides it with 100dvh; browsers that understand the second declaration use it, and the rest ignore it. 100svh is the alternative when the layout should never resize as toolbars move — it always uses the small viewport, leaving a gap when toolbars retract but never jumping. dvh, svh and lvh compares the three in detail.
Keeping the focused field clear of other chrome
Even when the browser scrolls a focused input into view, it only guarantees that the input itself is visible, not the send button next to it or a hint below it. Two small additions help. scroll-margin-block-end on the composer's input asks the browser to leave extra room below the field when bringing it into view, so a helper line or button row underneath stays visible. And for forms with a sticky header, scroll-padding-block-start on the scrolling container stops focused fields from landing underneath the header. scroll-margin and scroll-padding for Sticky Headers explains both properties.
Opting into overlays: the keyboard environment variables
Chromium-based browsers offer a third route through the VirtualKeyboard API. Script opts in with navigator.virtualKeyboard.overlaysContent = true, after which the keyboard overlays the page without resizing anything, and CSS receives the keyboard's geometry through environment variables such as env(keyboard-inset-height):
.chat__composer {
/* 0px when the keyboard is closed or the API is unavailable. */
margin-block-end: env(keyboard-inset-height, 0px);
}
This gives complete control — the composer can animate up with the keyboard, for instance — but it only works where the API exists, and the page must handle everything the browser would otherwise have done. For most layouts, resizes-content plus a grid layout is simpler and degrades better. Treat the API as a progressive enhancement for apps that need precise keyboard-aware positioning.
Testing on real devices
Desktop DevTools device emulation does not simulate the on-screen keyboard, so keyboard behaviour must be checked on real phones or a platform emulator that renders a keyboard. Check three things on each target browser: the focused input is visible while typing, the header or any other context stays where the design expects, and closing the keyboard restores the layout without leaving a gap. Test in both portrait and landscape; in landscape the keyboard can cover most of the screen, and a layout that needs more than the remaining height will scroll regardless of the techniques above. Remote debugging lets you inspect the live layout on the phone while the keyboard is open, which makes it easy to see which viewport has changed size.
Browser support
dvh, svh and lvh units are supported in Chrome and Edge 108+, Firefox 101+ and Safari 15.4+. The interactive-widget viewport meta value and the VirtualKeyboard API with its keyboard-inset-* environment variables are not in the verified support table this site uses; at the time of writing they are available in Chromium-based browsers, with other engines behaving according to their own defaults. Because unsupported values are ignored, the grid-based layout above works everywhere and improves where they are supported. safe-area-inset-* values are covered in Safe Area Insets for Notched Screens.
FAQ
Why does my fixed bottom bar hide behind the mobile keyboard? By default most mobile browsers overlay the keyboard on the page or resize only the visual viewport, leaving the layout viewport, and anything fixed to its bottom, where it was. The bar stays positioned against a viewport that is now partly covered.
What does interactive-widget=resizes-content do?
It is a value in the viewport meta tag that asks the browser to shrink the layout viewport when the on-screen keyboard opens. Fixed elements and dvh-based heights then adapt to the space above the keyboard. Browser support varies, so treat it as an enhancement.
Do dvh units account for the keyboard?
Only when the browser resizes the layout viewport for the keyboard, for example with interactive-widget=resizes-content. By default, dynamic viewport units track browser toolbars appearing and disappearing, not the keyboard.
What is env(keyboard-inset-height)? An environment variable exposed by the VirtualKeyboard API in Chromium-based browsers when a page opts in to overlaying the keyboard. It gives the keyboard's height so CSS can pad or reposition content. Other browsers do not provide it, so always include a fallback.
Related
- Viewport Units & Mobile Layout — the parent guide.
- dvh, svh and lvh: Mobile Viewport Units — the units used here.
- Overscroll Behavior for Nested Scrolling — keeping the thread's scroll contained.
- Sticky Footer With Flexbox and Grid — the same row structure on desktop.
Related articles
More pages in the same section.