viewport-units-fix
v1.0.0
Published
Reliable vh/vw on mobile. Injects --vh and --vw CSS variables that match the real visible viewport, updated on resize and orientation change. Zero dependencies.
Maintainers
Readme
📐 viewport-units-fix
Reliable vh/vw on mobile. Injects
--vhand--vwCSS variables that match the real visible viewport, updated on resize and orientation change. Zero dependencies.
Why
100vh on mobile Safari doesn't mean what you think. The browser's address bar and toolbar are part of the viewport, so a 100vh element extends behind the chrome — clipped and broken. It's one of the most-upvoted CSS issues on StackOverflow. In 2025, dvh exists but support is incomplete and its behaviour during scroll transitions is inconsistent.
Developers resort to CSS hacks, window.innerHeight calculations, or scattered one-off solutions that break in specific orientations, don't update on resize, or fail during SSR. None of them are drop-in.
viewport-units-fix makes this stop being a problem. One import, four CSS variables (--vh, --vw, --dvh, --dvw) that are always correct. Powered by visualViewport with innerHeight fallback, debounced updates, SSR safe. ~0.7kB gzipped.
Install
npm install viewport-units-fix
# or
yarn add viewport-units-fix
# or
pnpm add viewport-units-fixQuick Start
Set it and forget it
import ViewportUnitsFix from 'viewport-units-fix';
new ViewportUnitsFix();Now use the CSS variables anywhere:
.hero {
height: calc(var(--vh) * 100); /* true full viewport */
}
.sidebar {
height: var(--dvh); /* also full viewport, as a single value */
}With options
import ViewportUnitsFix from 'viewport-units-fix';
const vf = new ViewportUnitsFix({
prefix: 'app-', // sets --app-vh, --app-vw, etc.
debounce: 50, // update speed in ms
variables: ['vh'], // only inject --vh
onUpdate: (values) => {
console.log(values.dvh); // full visible height in px
},
});Features
- Zero dependencies — pure TypeScript, no external packages
- Four CSS variables —
--vh,--vw(1% units) and--dvh,--dvw(full pixel values) - visualViewport API — uses the modern
window.visualViewportfor accurate measurements, falls back toinnerHeight/innerWidth - Debounced updates — configurable delay (default 100ms) so resize floods don't thrash the DOM
- Orientation change — handles portrait/landscape transitions automatically
- iOS Safari aware — accounts for the address bar and safe area on real devices
- Custom prefix — namespace your variables with
prefix: 'app-'to avoid conflicts - Variable filtering — only inject the ones you need with the
variablesoption - Flexible targeting — set variables on
documentElement, a specific element, or a CSS selector onUpdatecallback — react to viewport changes in JS, not just CSS- SSR safe — guards all DOM access behind
typeof window - ~0.7kB minified + gzipped
API
new ViewportUnitsFix(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 milliseconds; 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 |
ViewportValues
| Property | Type | Description |
|----------|------|-------------|
| vh | number | 1% of the visible viewport height |
| vw | number | 1% of the visible viewport width |
| dvh | number | Full visible viewport height in pixels |
| dvw | number | Full visible viewport width in pixels |
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) |
Instance methods
| Method | Returns | Description |
|--------|---------|-------------|
| .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 |
Instance properties
| Property | Type | Description |
|----------|------|-------------|
| .values | ViewportValues | Read-only snapshot of the current measurements |
Examples
React hook
import { useEffect } from 'react';
import ViewportUnitsFix from 'viewport-units-fix';
function useViewportUnits() {
useEffect(() => {
const vf = new ViewportUnitsFix();
return () => vf.destroy();
}, []);
}
// In your app
function App() {
useViewportUnits();
return <div style={{ height: 'calc(var(--vh) * 100)' }}>Full screen</div>;
}Vue composable
import { onMounted, onUnmounted } from 'vue';
import ViewportUnitsFix from 'viewport-units-fix';
export function useViewportUnits() {
let vf: ViewportUnitsFix;
onMounted(() => {
vf = new ViewportUnitsFix();
});
onUnmounted(() => vf?.destroy());
}Scoped to a container
import ViewportUnitsFix from 'viewport-units-fix';
const vf = new ViewportUnitsFix({
target: '#app-shell',
prefix: 'app-',
});#app-shell .hero {
height: calc(var(--app-vh) * 100);
}CDN (no build step)
<script type="module">
import ViewportUnitsFix from 'https://esm.sh/viewport-units-fix';
new ViewportUnitsFix();
</script>
<style>
.hero {
height: calc(var(--vh) * 100);
}
</style>