Skip to content
Back to skills

Mobile Keyboard Viewport

ASecurity

Fix the iOS keyboard "composer stranded at the top / page scrolls up" bug in any mobile web app with a bottom-fixed input. Use whenever you build or touch a chat/message/comment/search UI whose input sits at the bottom of a full-height layout.

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 5, 2026
developmentgonextjs

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 5, 2026

npx -y skills add Podbay-Cloud/podbay --skill mobile-keyboard-viewport --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mobile Keyboard Viewport?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mobile Keyboard Viewport
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/podbay-cloud-mobile-keyboard-viewport/badge)](https://www.skillsdirectory.com/skills/podbay-cloud-mobile-keyboard-viewport)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mobile-keyboard-viewport
description: Fix the iOS keyboard "composer stranded at the top / page scrolls up" bug in any mobile web app with a bottom-fixed input. Use whenever you build or touch a chat/message/comment/search UI whose input sits at the bottom of a full-height layout.
---

# Mobile keyboard viewport fix (iOS)

## When to use
Any mobile-first web UI with a **bottom-pinned input** in a **full-height** layout
(chat, messaging, comments, a bottom search bar). Symptom to prevent: on iOS
Safari and in-app webviews, tapping the input opens the keyboard and the page
"scrolls up" — the input ends up stranded near the top with dead space below it,
and content is pushed off-screen. `100dvh`/`100vh` does NOT fix this.

## Why it happens
Mobile browsers keep two viewports: the **layout** viewport (CSS lays out against
it) and the **visual** viewport (what's actually visible). When the keyboard opens
iOS (a) shrinks the visual viewport while `100dvh` keeps tracking the layout
viewport, and (b) **pans** the document to reveal the focused input, sliding a
top-pinned app off-screen. CSS units can't opt out — you must size and position
from `window.visualViewport` and lock the document.

## The fix (Next.js App Router; adapt to any framework)

**1. `app/layout.tsx`** — the Android half (keyboard resizes, not overlays):
```ts
import type { Viewport } from "next";
export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
  interactiveWidget: "resizes-content",
};
```

**2. `globals.css`** — lock the document; size + translate the app from CSS vars:
```css
html, body { height: 100%; }
body { position: fixed; inset: 0; overflow: hidden; overscroll-behavior: none; }
.app {                                     /* the full-height flex column */
  height: 100dvh;                          /* pre-JS / desktop fallback */
  height: var(--app-h, 100dvh);            /* real visible height */
  transform: translateY(var(--app-y, 0px)); /* follow the visual-viewport pan */
}
/* ONLY the scrollable region (e.g. the message list) scrolls: */
.messages-scroll { flex: 1; overflow-y: auto; }
```

**3. The client component** — sync the vars from `visualViewport`:
```ts
useEffect(() => {
  const vv = window.visualViewport;
  if (!vv) return;
  const sync = () => {
    const root = document.documentElement;
    root.style.setProperty("--app-h", `${vv.height}px`);
    root.style.setProperty("--app-y", `${vv.offsetTop}px`); // ← the piece most fixes miss
    window.scrollTo(0, 0);                                   // undo residual pan
    scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }); // keep newest visible
  };
  sync();
  vv.addEventListener("resize", sync);
  vv.addEventListener("scroll", sync); // iOS PANS without firing 'resize' — listen to scroll too
  return () => {
    vv.removeEventListener("resize", sync);
    vv.removeEventListener("scroll", sync);
  };
}, []);
```

## Non-negotiables / gotchas
- **`translateY(offsetTop)`** is the piece most fixes miss: even with a fixed body,
  iOS still pans the visual viewport. Translating by `offsetTop` keeps the app
  glued to what's visible.
- **Listen to `scroll`, not just `resize`** — iOS pans without resizing in several
  cases (in-app webviews are the worst offenders).
- **Composer input `font-size: 16px` (never smaller).** Below 16px iOS Safari
  auto-zooms the page on focus — a separate bug with an identical-looking symptom
  that compounds this one. (Best set globally: `@media (max-width:640px){
  input,textarea,select{font-size:16px !important} }`.)
- Ship BOTH halves: `interactiveWidget` is Android, the `visualViewport` sync is iOS.

## Verify
On an iPhone (Safari AND an in-app webview like the Google app — webviews are
worse): tap the input. The composer should stay pinned just above the keyboard
with the scroll region compressed above it — no dead space below, nothing pushed
off-screen. Dismiss the keyboard → full height returns. Rotate with the keyboard
open. On desktop, the layout is unchanged (the vars are unset → `100dvh`, no
translate).

Files in this skill

  • SKILL.md4.1 KB
  • SOURCE.md349 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.