Fixing the Mobile Keyboard Covering Input Fields on Mobile Web
A chat input or form field pinned to the bottom of the screen works fine until the on-screen keyboard opens — then it's either hidden entirely behind the keyboard or the page scrolls to somewhere unhelpful. Native apps keep the focused field visible automatically; the mobile browser doesn't, and CSS position:fixed alone doesn't know the keyboard exists.
The Problem
A text input pinned near the bottom of the screen — a chat message box, a comment field, a fixed bottom form bar — works correctly until a user actually taps it on a real mobile device. The on-screen keyboard opens, and the input either ends up hidden behind the keyboard entirely, or the page scrolls in a way that leaves the focused field at an awkward, partially visible position. This is especially persistent on iOS Safari, and often doesn't reproduce at all in a desktop browser's mobile device emulation, which makes it easy to ship without noticing.
Why It Happens
Mobile browsers don't all handle the keyboard's effect on the viewport the same way
When the on-screen keyboard opens, it visually covers part of the screen — but different mobile browsers represent that differently at the CSS layer. Some resize the actual layout viewport so elements reflow around the smaller visible area; others keep the layout viewport reporting its original size and simply render the keyboard as an overlay on top of the page. An element positioned with position: fixed relative to "the bottom of the viewport" can end up anchored to a viewport height that no longer matches what's actually visible above the keyboard, depending on which behavior the browser in question uses.
iOS Safari's own browser chrome compounds the mismatch further
iOS Safari's address bar can independently show or hide as part of scrolling, on top of the keyboard's own show/hide behavior — the same layout-viewport-versus-visual-viewport gap that causes 100vh sizing bugs shows up here too, but triggered specifically by keyboard appearance rather than scroll-driven chrome collapse. A fixed-position element at the bottom of the screen is a particularly common casualty of this compounding effect.
Focus and "keyboard fully open" aren't the same synchronous moment
A page that relies purely on default browser scrolling behavior when an input receives focus can leave that input exactly at the boundary the keyboard is about to cover, because the keyboard's own opening animation hasn't necessarily finished by the time the focus event fires. Code that calls scrollIntoView immediately on focus, before the keyboard has actually finished animating into place, can scroll to a position that's correct before the keyboard opens and wrong the instant after.
The Fix
1. Use the VisualViewport API to react to the keyboard's actual effect on visible space
function handleViewportResize() {
const vv = window.visualViewport;
const inputBar = document.querySelector(".bottom-input-bar");
inputBar.style.transform = `translateY(${window.innerHeight - vv.height - vv.offsetTop}px)`;
}
window.visualViewport?.addEventListener("resize", handleViewportResize);
window.visualViewport reports the actual visible viewport, distinct from the layout viewport — this is the API specifically designed to answer "how much space does the keyboard actually leave visible right now," and reacting to its resize event lets a fixed element reposition based on real available space instead of a static assumption.
2. Delay scrolling the focused input into view until the keyboard has settled
input.addEventListener("focus", () => {
window.visualViewport?.addEventListener(
"resize",
() => input.scrollIntoView({ block: "center", behavior: "smooth" }),
{ once: true }
);
});
Scrolling in response to the visual viewport's resize event, rather than immediately on focus, waits for the keyboard's own animation to actually finish changing the visible area before deciding where the input needs to be — closing the timing gap that causes a scroll calculated too early to land in the wrong place.
3. Anchor bottom-pinned bars to the VisualViewport's real dimensions, not a static position:fixed assumption
For an input bar that must stay pinned above the keyboard specifically, compute its position from visualViewport.height and visualViewport.offsetTop on every resize event rather than relying on plain CSS position: fixed; bottom: 0, which has no awareness of the keyboard at all and anchors to whatever the layout viewport happens to report.
4. Test on real iOS Safari and Android Chrome, not just desktop device emulation
Desktop browser mobile emulation resizes the viewport for testing responsive layout, but it doesn't reproduce an actual on-screen keyboard's show/hide behavior or its interaction with browser chrome — this specific class of bug reliably shows correct in emulation and broken on a real device, so verification needs an actual phone or a real-device cloud testing service, not just a resized desktop window.
Why This Works
Each fix relies on the API specifically built to answer the question a static CSS layout can't: how much of the screen is actually visible right now, given the keyboard's current state. The VisualViewport API reports that directly rather than requiring an inference from layout viewport dimensions that may not reflect it; timing the scroll to the viewport's resize event rather than the focus event accounts for the real animation delay between the two; and testing on real devices catches the gap between emulated and actual keyboard behavior before it reaches users.
Conclusion
A mobile keyboard covering a focused input, or a fixed bottom bar ending up in the wrong place, is a consequence of the layout viewport not reliably reflecting what the keyboard actually leaves visible — not something fixable with CSS positioning alone. Use the VisualViewport API to track the real visible area and reposition fixed elements against it, delay scrolling a focused input until the keyboard's resize event actually fires, and verify the fix on real iOS Safari and Android Chrome devices rather than trusting desktop emulation.