Source Code

<div class="demo">
  <div class="phone-frame">
    <div class="chat-screen" id="chatScreen">
      <div class="chat-header">Support chat</div>
      <div class="chat-messages" id="chatMessages">
        <div class="msg them">Hi! How can we help today?</div>
        <div class="msg me">My export keeps failing at 90%.</div>
        <div class="msg them">Got it — checking that now. On a phone, try focusing the input below: it stays pinned above the (simulated) keyboard instead of sliding underneath it.</div>
      </div>
      <form class="chat-input-bar" id="chatInputBar">
        <input type="text" id="chatInput" placeholder="Type a message…" autocomplete="off" />
        <button type="submit" class="chat-send" aria-label="Send message">
          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
        </button>
      </form>
    </div>
  </div>
  <p class="vv-hint">On a real mobile browser, VisualViewport tracks the software keyboard's height directly, so the input bar sits exactly above it — never hidden underneath.</p>
</div>

Keyboard-Safe Fixed Input Bar — Correct Mobile Keyboard Handling with VisualViewport

Keyboard-Safe Fixed Input Bar with the VisualViewport API · Mobile · Plain HTML, CSS & JS · Live preview

What's included

Features

Uses the VisualViewport API to detect the real space consumed by the mobile keyboard, not a guess based on focus timing alone
Correctly distinguishes the layout viewport from the visual viewport — the root cause of the fixed-bar-hidden-behind-keyboard bug
Repositions via an additive transform: translateY() rather than fighting the element's existing base positioning
Listens to both visualViewport resize and scroll events, covering keyboard open/close and any viewport panning
Small delay on focus avoids reading a stale, pre-animation viewport height on some mobile browsers
Gracefully no-ops on browsers without VisualViewport support rather than throwing an error
Respects env(safe-area-inset-bottom) so the bar also clears the home-indicator area on notched devices when the keyboard is closed

About this UI Snippet

Keyboard-Safe Input Bar — Solving the Fixed-Position-Underneath-the-Keyboard Bug

Screenshot of the Keyboard-Safe Fixed Input Bar with the VisualViewport API snippet rendered live

Every mobile chat, messaging, or comment UI eventually hits the same frustrating bug: a position: fixed input bar pinned to the bottom of the screen, which — the moment the on-screen keyboard opens — ends up rendered *underneath* that keyboard, invisible, even though CSS still believes it's correctly anchored to the bottom of the viewport. This snippet fixes it properly using the VisualViewport API, the browser API specifically designed to report what's actually visible on screen.

Why `position: fixed` alone isn't enough

There are two different "viewports" a mobile browser tracks: the layout viewport (what CSS position: fixed, vh units, and window.innerHeight generally measure against) and the visual viewport (the actual currently-visible region of the screen, which shrinks when the keyboard opens). On many mobile browsers, opening the keyboard shrinks the *visual* viewport while leaving the *layout* viewport's dimensions unchanged — so a bar fixed to the bottom of the layout viewport stays exactly where it was, now sitting behind the keyboard rather than above it.

`window.visualViewport` reports the truth, and fires `resize` when it changes

repositionInputBar() computes keyboardHeight as the difference between window.innerHeight (the layout viewport) and visualViewport.height plus its offsetTop — this gap is exactly the space currently consumed by the keyboard (or any other viewport-shrinking browser chrome). The visualViewport's own resize event fires precisely when the keyboard opens, closes, or changes height, giving the exact signal needed to know when to recalculate, rather than guessing based on focus/blur timing alone.

Why `transform: translateY()`, not changing `bottom`

The fix nudges the bar up using transform: translateY(-Npx) rather than adjusting its bottom CSS property. This is deliberate: the bar's base position is already established through normal fixed/flex positioning; layering a transform-based offset on top is an *additive* adjustment that doesn't need to fight or duplicate that base positioning logic — it simply shifts the already-correctly-positioned element up by exactly the keyboard's height, then removes the transform entirely (returning to the base position) the instant the keyboard closes.

A short delay on focus, to avoid reading a stale viewport size

The focus handler doesn't call repositionInputBar() synchronously — it wraps it in a small setTimeout. On some mobile browsers, the visual viewport's dimensions haven't finished updating at the exact instant a focus event fires (the keyboard's opening animation is still in progress), so checking immediately can read a stale, pre-resize height. The short delay gives the keyboard animation a moment to actually complete its resize before the code measures it.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain in detail the difference between the layout viewport and the visual viewport on mobile browsers, and why this distinction is specifically what causes fixed-position elements to end up hidden behind an open keyboard. It's also worth asking for a version that additionally auto-scrolls the currently-focused input into view above the repositioned bar, or one that smoothly animates the keyboard-avoidance transform using the same duration as the platform's native keyboard animation for a more seamless feel.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a keyboard-safe, fixed-position chat input bar in HTML, CSS, and vanilla JavaScript using the VisualViewport API — no external library.

Requirements:
- A chat-style mobile UI with a scrollable message list and a fixed input bar (text input plus send button) pinned to the bottom of the screen.
- Use window.visualViewport to detect the real height currently occupied by the on-screen keyboard, computed as the difference between the layout viewport (window.innerHeight) and the visual viewport's actual height and offset — do not rely solely on guessing based on input focus/blur timing.
- Listen to the visualViewport's resize event (and its scroll event, to handle viewport panning) to recalculate and reposition the input bar whenever the visible keyboard area changes.
- Reposition the bar using an additive CSS transform: translateY() based on the detected keyboard height, rather than modifying its base bottom/top positioning — the transform should shift the bar up by exactly the keyboard's height and reset to none when the keyboard is closed.
- On input focus, add a brief delay before the first repositioning check, to avoid reading a stale visual viewport height before the keyboard's opening animation has finished resizing it.
- Gracefully handle browsers that don't support VisualViewport by skipping the repositioning logic entirely rather than throwing an error, so the bar still falls back to normal fixed-position behavior.
- Respect env(safe-area-inset-bottom) padding on the bar for devices with a home-indicator safe area.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Open this snippet on an actual mobile deviceThe effect is only visible on real mobile browsers with a software keyboard — desktop browsers have no keyboard to resize the viewport.
  2. 2
    Tap the message inputThe keyboard opens, and the input bar smoothly translates upward to sit exactly above it, remaining fully visible rather than sliding underneath.
  3. 3
    Dismiss the keyboardThe bar's transform resets to none, and it returns to its normal fixed position at the bottom of the screen.
  4. 4
    Send a messageThe message list scrolls to the newest message, demonstrating the pattern working inside a realistic chat UI context.
  5. 5
    Adapt repositionInputBar() to your own fixed barPoint the function at your own bottom-fixed element and wire the same visualViewport resize/scroll listeners to it.

Real-world uses

Common Use Cases

CHAT
Chat and messaging interfaces
The exact scenario this snippet demonstrates — a message input that must stay visible and usable while the keyboard is open.
COMMENT
Comment and reply composer bars
Any bottom-fixed comment or reply input on a mobile web app needs the same keyboard-safe repositioning.
SEARCH
Mobile search bars
A persistent search input pinned to the bottom of a mobile layout benefits from staying visible above the keyboard while typing.
Mobile checkout or quick-entry forms
Any mobile form with a fixed action bar (like a "continue" button paired with an input) needs correct keyboard-aware positioning.
Related: Sticky Footer CTA Bar with Safe-Area Insets
See the Sticky Footer CTA Bar with Safe-Area Insets for a related mobile pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Mobile browsers track two different viewports — a layout viewport that position: fixed measures against, and a visual viewport representing what's actually currently visible. Opening the keyboard shrinks the visual viewport while often leaving the layout viewport's size unchanged, so a naively fixed bar stays where the layout viewport says the bottom is, which is now hidden underneath the keyboard.

It reports the real, currently-visible viewport dimensions and position, and fires a resize event whenever that visible area changes — including specifically when the on-screen keyboard opens, closes, or changes height, which is exactly the signal this pattern needs.

The bar already has a correct base position from normal fixed/flex layout. Using transform: translateY() as an additive adjustment on top of that base position avoids needing to duplicate or override the existing positioning logic — it simply shifts the already-correct position up by exactly the keyboard's height.

On some mobile browsers, the visual viewport hasn't finished resizing at the exact moment a focus event fires — the keyboard's opening animation may still be in progress. A short setTimeout gives that resize a moment to actually complete before the height is measured, avoiding a stale reading.

The code checks for window.visualViewport before using it and simply no-ops (skips the repositioning logic entirely) if it's unavailable, falling back to the bar's normal fixed-position behavior rather than throwing an error.

Yes — the input bar's padding-bottom uses max(10px, env(safe-area-inset-bottom)), ensuring it clears the home-indicator gesture area on notched devices when the keyboard is closed, independent of the keyboard-avoidance transform logic.