Skip to content

rl0425/use-dynamic-viewport

Repository files navigation

use-dynamic-viewport

npm version npm downloads bundle size license TypeScript

React hook that injects --dvh and --keyboard-height CSS variables using the Visual Viewport API — fixing the gap CSS dvh leaves when the mobile keyboard opens.


The problem

CSS dvh (dynamic viewport height) was introduced to replace the infamous 100vh bug on mobile. It responds to the browser URL bar appearing and disappearing — but it does not update when the on-screen keyboard opens.

/* This still breaks when the keyboard opens */
.chat-input-bar {
  position: fixed;
  bottom: 0;
  height: 60px;
}

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

The reliable fix requires window.visualViewport:

keyboardHeight = layoutHeight - window.visualViewport.height

The hook captures window.innerHeight at mount time as a stable reference before any keyboard interaction. When the keyboard opens, window.visualViewport.height shrinks — the difference between the captured reference and the current vv.height is the keyboard height.

Note: the hook applies two guards to protect the baseline from being overwritten by a keyboard event:

  • iOS Safari — when the keyboard opens, visualViewport.offsetTop increases (the visual viewport scrolls). The hook skips the update when offsetTop > 0.
  • Android Chrome — when the keyboard opens, the layout viewport shrinks but offsetTop stays 0. The hook skips the update when innerWidth is unchanged, because the keyboard never changes the viewport width (only orientation changes do).

This hook wraps that logic and injects two CSS variables automatically.


Installation

npm install use-dynamic-viewport

Requires React 17 or later. Zero runtime dependencies.


Quick start

import { useDynamicViewport } from 'use-dynamic-viewport'

export function Layout() {
  useDynamicViewport() // injects --dvh and --keyboard-height

  return <div className="app">{/* ... */}</div>
}
/* Use --dvh instead of 100vh */
.fullscreen {
  height: var(--dvh, 100vh);
}

/* Fixed bottom bar that stays above the keyboard */
.chat-input {
  position: fixed;
  bottom: var(--keyboard-height, 0px); /* moves entire element above the keyboard */
  left: 0;
  right: 0;
}

API

useDynamicViewport(options?)

const { viewportHeight, keyboardHeight, isKeyboardOpen } = useDynamicViewport(options?)

Options

Option Type Default Description
heightVar string '--dvh' CSS custom property name for the visual viewport height
keyboardVar string '--keyboard-height' CSS custom property name for the keyboard height
enabled boolean true Set to false to disable the hook entirely

Return value

Property Type Description
viewportHeight number Current visual viewport height in pixels
keyboardHeight number Current keyboard height in pixels (0 when closed)
isKeyboardOpen boolean true when the on-screen keyboard is open

Examples

Basic — just inject the CSS variables

useDynamicViewport()

Read values in JavaScript

const { viewportHeight, keyboardHeight, isKeyboardOpen } = useDynamicViewport()

return (
  <div>
    {isKeyboardOpen && <p>Keyboard is open ({keyboardHeight}px)</p>}
  </div>
)

Custom CSS variable names

useDynamicViewport({
  heightVar: '--vh',
  keyboardVar: '--kb-height',
})
.page { height: var(--vh); }
.footer { padding-bottom: var(--kb-height); }

Conditional activation

const isMobile = useMediaQuery('(max-width: 768px)')
useDynamicViewport({ enabled: isMobile })

How it works

CSS injected on <html>

:root {
  --dvh: 780px;         /* actual visible height (URL bar + keyboard aware) */
  --keyboard-height: 0px; /* 0 when closed, ~300px when keyboard is open */
}

Variables are updated on every visualViewport resize and scroll event (both are needed for iOS Safari compatibility), throttled with requestAnimationFrame.

Cleanup

Variables are removed from document.documentElement when the component unmounts. Event listeners are also cleaned up.

SSR safety

window access is guarded by typeof window === 'undefined' — safe for Next.js App Router, Remix, and any SSR environment.


Comparison

Feature use-dynamic-viewport Manual visualViewport viewportify
CSS --dvh variable ❌ manual
CSS --keyboard-height variable ❌ manual
React hook (first-class) ⚠️ wrapper
Next.js App Router / SSR safe ⚠️
iOS Safari scroll event compat ⚠️ easy to miss
Zero dependencies
Bundle size ~0.8KB n/a larger
Cleanup on unmount ❌ manual

Known limitations

  • Keyboard height detection uses the window.innerHeight captured at mount time as a baseline. When window.visualViewport.height shrinks below that baseline, the difference is treated as keyboard height. Two device behaviors are handled:
    • iOS Safari — visual viewport scrolls when keyboard opens (vv.offsetTop > 0). The hook detects this and preserves the baseline.
    • Android Chrome — layout viewport itself shrinks (window.innerHeight decreases, vv.offsetTop stays 0). The hook detects this via a width guard: keyboard open never changes innerWidth, so the baseline is only updated when innerWidth changes (true orientation change or window resize).
    • Niche Android browsers that behave differently from these two patterns may not be fully supported.
  • On desktop, --keyboard-height is 0px in normal use. If the browser window height is resized without changing its width (e.g. dragging the bottom edge), the variable may briefly show a non-zero value. This has no practical impact since desktop browsers have no on-screen keyboard.
  • The hook injects variables on document.documentElement. If you render multiple instances, the last mounted instance controls the variables.
  • Pinch-to-zoom shrinks visualViewport.height, which can produce a false positive --keyboard-height. Most mobile apps disable zoom via <meta name="viewport" content="..., maximum-scale=1">, which avoids this.
  • Dynamic CSS variable names: if you change heightVar or keyboardVar at runtime, the old variable name is not automatically removed from :root. The old variable lingers until the component unmounts. Prefer stable variable names.
  • iPhone home indicator (safe-area-inset): on devices with a home bar, combine --keyboard-height with env(safe-area-inset-bottom) to avoid the bottom safe area:
.input-bar {
  padding-bottom: calc(var(--keyboard-height, 0px) + env(safe-area-inset-bottom, 0px));
}

Browser support

Requires window.visualViewport (95%+ global support). Falls back to window.innerHeight when not available, meaning --keyboard-height will always be 0px in unsupported browsers.


License

MIT © parkgichan

About

React hook that fixes the gap CSS dvh leaves when the mobile keyboard opens — injects --dvh and --keyboard-height CSS variables via the Visual Viewport API

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors