DEV Community

Gichan
Gichan

Posted on • Originally published at dev.to

Why CSS dvh ignores the mobile keyboard — and how to fix it

CSS dvh (dynamic viewport height) was introduced to replace the infamous 100vh bug on mobile. It handles the browser URL bar appearing and disappearing — but it completely ignores the on-screen keyboard.

When the keyboard opens, position: fixed elements get covered. dvh won't help you here.

Why dvh ignores the keyboard

The CSS viewport units spec treats the virtual keyboard as an overlay — it doesn't resize the layout viewport. So dvh, svh, and 100vh all stay the same value when the keyboard opens.

The real fix: Visual Viewport API

keyboardHeight = window.innerHeight - window.visualViewport.height
Enter fullscreen mode Exit fullscreen mode

When the keyboard opens, visualViewport.height shrinks while window.innerHeight stays fixed. The difference is the keyboard height.

But there's a catch — iOS Safari and Android Chrome behave differently.

iOS Safari

When the keyboard opens, iOS scrolls the visual viewport upward. visualViewport.offsetTop increases. It fires both resize and scroll events on visualViewport — but window resize does not fire.

Android Chrome

Android shrinks the layout viewport itself. window.innerHeight decreases and window.resize fires. visualViewport.offsetTop stays 0.

If you only handle one case, the other breaks.

The guard logic

const handleWindowResize = () => {
  const vv = window.visualViewport
  const currWidth = window.innerWidth
  const widthChanged = currWidth !== layoutWidthRef.current

  // iOS: vv.offsetTop > 0 when keyboard is open → skip
  // Android: width unchanged when keyboard opens → skip
  // True orientation/resize: width changes → update reference
  if (!vv || (vv.offsetTop === 0 && widthChanged)) {
    layoutHeightRef.current = window.innerHeight
    layoutWidthRef.current = currWidth
  }
}
Enter fullscreen mode Exit fullscreen mode

In testing, keyboard opens didn't affect innerWidth, while orientation changes did. This is the heuristic that makes the width guard reliable for Android.

The hook

I wrapped this into use-dynamic-viewport, which injects two CSS variables automatically:

npm install use-dynamic-viewport
Enter fullscreen mode Exit fullscreen mode
import { useDynamicViewport } from 'use-dynamic-viewport'

export function Layout() {
  useDynamicViewport() // injects --dvh and --keyboard-height on :root
  return <div className="app">...</div>
}
Enter fullscreen mode Exit fullscreen mode
/* App height tracks the visible area */
.app {
  height: var(--dvh, 100svh);
}

/* Fixed bottom bar stays above the keyboard */
.input-bar {
  position: fixed;
  bottom: var(--keyboard-height, 0px);
  left: 0;
  right: 0;
}
Enter fullscreen mode Exit fullscreen mode

You can also read the values in JS:

const { viewportHeight, keyboardHeight, isKeyboardOpen } = useDynamicViewport()
Enter fullscreen mode Exit fullscreen mode

Details

  • iOS Safari ✅ Android Chrome ✅
  • Next.js App Router / SSR safe ✅
  • Zero dependencies, ~0.8KB gzipped
  • React 17+, TypeScript
  • 19 tests (Vitest + React Testing Library)

GitHub: https://github.com/rl0425/use-dynamic-viewport
npm: https://www.npmjs.com/package/use-dynamic-viewport

Top comments (0)