Zero dependencies · TypeScript · ~0.7kB gzip

Make vh actually
work on mobile

One import. Four CSS variables — --vh, --vw, --dvh, --dvw — that match the real visible viewport. Always.

✦ See it live
visualViewport API iOS Safari Orientation change SSR safe ~0.7kB gzip
Why viewport-units-fix

Everything you need, nothing you don't

📱

Mobile Safari fixed

The address bar and toolbar no longer clip your 100vh layouts. The variables always reflect the real visible area.

🎯

Four CSS variables

--vh and --vw for 1% units, --dvh and --dvw for full pixel values. Use whichever fits your CSS.

visualViewport API

Uses the modern window.visualViewport for accurate measurements, with innerHeight fallback.

🔄

Auto-updating

Debounced resize and orientation change listeners keep the variables in sync. Configurable delay (default 100ms).

🏷️

Namespacing

Add a custom prefix like prefix: 'app-' to avoid conflicts. Only inject the variables you need.

🚫

Zero dependencies

Pure TypeScript. SSR safe. Drop it into any framework or vanilla project. ~0.7kB gzipped.

Interactive Demo

See the difference

The classic mobile Safari problem, visualized

On the left, height: 100vh extends behind the browser chrome. On the right, height: calc(var(--vh) * 100) fits perfectly.

height: 100vh
mysite.com
😵 Content clipped! height: 100vh
hidden behind toolbar
height: calc(var(--vh) * 100)
mysite.com
Fits perfectly height: calc(var(--vh) * 100)
--vh
1% height
--vw
1% width
--dvh
full height
--dvw
full width
↔ Resize your browser to see values update in real time

Live preview: this box uses height: calc(var(--vh) * 20)

20% of real viewport
Get Started

Install in 30 seconds

npm install viewport-units-fix
yarn add viewport-units-fix
pnpm add viewport-units-fix
<script type="module">
  import ViewportUnitsFix from 'https://esm.sh/viewport-units-fix';
  new ViewportUnitsFix();
</script>
import ViewportUnitsFix from 'viewport-units-fix';

new ViewportUnitsFix();

// That's it. --vh, --vw, --dvh, --dvw are now on :root
import ViewportUnitsFix from 'viewport-units-fix';

const vf = new ViewportUnitsFix({
  prefix: 'app-',       // --app-vh, --app-vw, etc.
  debounce: 50,          // faster updates
  variables: ['vh'],     // only inject --vh
  onUpdate: (values) => {
    console.log(values.dvh); // full height in px
  },
});
import { useEffect } from 'react';
import ViewportUnitsFix from 'viewport-units-fix';

function useViewportUnits() {
  useEffect(() => {
    const vf = new ViewportUnitsFix();
    return () => vf.destroy();
  }, []);
}
/* Full viewport height — the fix for 100vh */
.hero {
  height: calc(var(--vh) * 100);
}

/* Or use the full-pixel value directly */
.sidebar {
  height: var(--dvh);
}

/* Half viewport */
.panel {
  height: calc(var(--vh) * 50);
}
API Reference

Constructor options

Option Type Default Description
variables ViewportVariable[] all Which CSS variables to inject: 'vh', 'vw', 'dvh', 'dvw'.
prefix string '' Prefix for CSS variable names (e.g. 'app-' → --app-vh).
debounce number 100 Debounce delay in ms. Set to 0 for immediate updates.
target HTMLElement | string document.documentElement Element or CSS selector to set CSS variables on.
onUpdate (values: ViewportValues) => void Called after each measurement update.

Instance methods & properties

Member Type Description
.values ViewportValues Read-only snapshot: { vh, vw, dvh, dvw }.
.refresh() void Force an immediate recalculation and CSS variable update.
.destroy() void Remove all event listeners and clean up.
[Symbol.dispose]() void Alias for destroy(). Enables using syntax.

CSS variables

Variable Value CSS Usage
--vh {height/100}px height: calc(var(--vh) * 100)
--vw {width/100}px width: calc(var(--vw) * 100)
--dvh {height}px height: var(--dvh)
--dvw {width}px width: var(--dvw)