npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-dynamic-viewport

v0.1.0

Published

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

Readme

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