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

postcss-viewport-fallback

v1.6.1

Published

PostCSS plugin that adds classic fallbacks for all dynamic/small/large viewport units (dvh, svh, lvh, dvw, dvmin, ...).

Readme

postcss-viewport-fallback

npm version npm downloads CI license node

The universal fallback layer for modern CSS viewport units — all 18 of them:

  • dvw, dvh, dvi, dvb, dvmin, dvmax (dynamic)
  • svw, svh, svi, svb, svmin, svmax (small)
  • lvw, lvh, lvi, lvb, lvmin, lvmax (large)

Two strategies, one plugin:

  • duplicate (default, zero-runtime) — inserts a classic vh/vw/... twin before the original declaration; old browsers use the twin, modern browsers use the real unit
  • css-var — rewrites units to calc(var(--pvf-dvh, 1vh) * N) with an auto-injected @supports seed, optionally paired with a ~1 KB (gzip) runtime that gives legacy browsers actually dynamic dv* values (sv*/lv* are approximated)

Works in calc(), @media/@supports/@container params, nested CSS, Tailwind output, Vue/Svelte SFCs.


Features

Core Transformation

  • All 18 viewport units — dynamic, small, and large × w/h/i/b/min/max
  • Two strategies: zero-runtime duplicate twins, or dynamic css-var + runtime
  • Works inside calc(), min(), max(), clamp(), and nested functions of any depth
  • Decimals, negative and signed values, scientific notation, case-insensitive units
  • Safe parsing with postcss-value-parser — never touches strings, url(), or idents
  • Fully configurable: preserve, onlyProperties, excludeProperties, customUnits, includeCustomProps
  • Performance: browserslist auto-skip, fastSkip per-file scan, dedup, concurrent-safe stats

At-Rule Support

Fallback generation inside:

  • @media
  • @supports
  • @container (including nested container queries)

At-rule example

@media (min-height: 100dvh) { ... }
/* becomes */
@media (min-height: 100vh) { ... }
@media (min-height: 100dvh) { ... }

Example

Input

.app {
  height: 100dvh;
  width: calc(100dvw - 20px);
}

Output

.app {
  height: 100vh;
  height: 100dvh;
  width: calc(100vw - 20px);
  width: calc(100dvw - 20px);
}

Do you still need this in 2026?

Dynamic viewport units are Baseline Widely Available (~93% global support: Chrome 108+, Safari 15.4+, Firefox 101+). For a typical US/EU consumer app the fallback is close to optional.

You do still need it if your browserslist includes any of:

  • Emerging-market browsers — Opera Mini (no support, ever), UC Browser, QQ, KaiOS
  • Smart-TV / digital-signage — Tizen & webOS pin Chromium per model year; 2016–2022 TVs never reach Chromium 108
  • Embedded WebViews / kiosks / car head units stuck on pre-108 Chromium
  • Long-tail / public-sector device matrices that keep old iOS & Android in scope

The honest pitch: if you don't need it, browserslist: true makes the plugin cost exactly zero. If you do, it's the most complete option available. See the comparison below.


Options

viewportFallback({
  strategy: 'duplicate',   // or 'css-var' (dynamic legacy values, pairs with the runtime)
  preserve: true,          // false removes original dvh, keeps only vh fallback
  includeCustomProps: false,
  onlyProperties: undefined,   // string | RegExp | Array<string | RegExp>
  excludeProperties: undefined,
  debug: false,            // or 'minimal' | 'verbose'
  strict: false,
  customUnits: undefined,  // e.g. { cqh: 'vh', cqw: 'vw' }
  browserslist: false,     // true | browserslist query — auto-skip if all targets support dvh
  fastSkip: false,         // skip files without viewport units via one regex scan
  onTransform(meta) {
    console.log(meta);
  },
  onComplete(stats) {
    console.log(stats); // { declarations, atRules, skipped, timeMs }
  },
});

Option Details

strategy

How declaration fallbacks are produced (default: 'duplicate').

'duplicate' — the classic, zero-runtime approach. Inserts a static twin declaration before the original:

/* height: 100dvh  →  */
height: 100vh;
height: 100dvh;

Old browsers keep the last value they understand (100vh); modern browsers use 100dvh. Nothing to ship, works everywhere. Best default for most projects.

'css-var' — dynamic fallback for legacy browsers. Rewrites each unit to a calc() over a CSS variable and injects a :root seed that upgrades to the real unit via @supports:

/* input */
.sheet { height: 100dvh; }

/* output */
:root { --pvf-dvh: 1vh }
@supports (height: 1dvh) { :root { --pvf-dvh: 1dvh } }
.sheet { height: calc(var(--pvf-dvh, 1vh) * 100); height: 100dvh; }

On its own this behaves like duplicate (falls back to 1vh). Its power comes from pairing it with the runtime (below), which sets --pvf-dvh from the actual viewport in browsers that lack dvh — so 100dvh follows the live viewport instead of the static vh approximation that ignores mobile browser chrome.

The --pvf- custom-property prefix is reserved by the plugin: declarations whose name starts with --pvf- are never transformed, and the runtime writes only those variables. Don't define your own --pvf-* properties.

At-rule params (@media (min-height: 100dvh)) always use the duplicate strategy, because var()/calc() are invalid in feature-query context.

The runtime (for strategy: 'css-var')

A dependency-free module (ESM: 2.9 KB raw / 1.0 KB gzip). Import it once at your app entry:

import { applyViewportVars } from 'postcss-viewport-fallback/runtime';

applyViewportVars(); // no-op in browsers that support dvh

In browsers with native dv* support it detects CSS.supports('height','1dvh') and does nothing (the @supports seed already handles them). In browsers without it, it measures the layout viewport (innerWidth/innerHeight), sets every --pvf-* variable to 1% of the live dimension, and honors vertical writing modes for vi/vb. Returns { update, destroy } for manual control and SPA teardown. SSR-safe (no-op without window).

Accuracy — honest limitations. Native sv*/lv* values cannot be reproduced exactly from JavaScript, so the runtime approximates:

  • dv* follow the live layout viewport — the closest JS can get to native behavior
  • sv*/lv* track the smallest/largest size seen so far in the current orientation (extremes reset on rotation), so they converge to native-like values only after the browser chrome has actually collapsed/expanded once
  • updates are frozen during pinch-zoom (visualViewport.scale !== 1), which must not affect viewport units
  • a same-width height collapse >30% while an input is focused is treated as the on-screen keyboard and ignored, matching native dv* semantics (heuristic — a deliberately resized window with a focused input can be misclassified)

Framework entry points:

// Vite / plain
import { applyViewportVars } from 'postcss-viewport-fallback/runtime';
applyViewportVars();

// Next.js — app/layout.tsx (client component) or a <Script> tag
'use client';
import { applyViewportVars } from 'postcss-viewport-fallback/runtime';
applyViewportVars();

If your target browsers all support dvh (or you're happy with the static vh approximation), stick with the default duplicate strategy and skip the runtime entirely.

preserve

Keep the original modern declaration alongside the fallback (default: true).

Set to false to output only the fallback — useful when targeting only older browsers:

// Input:  height: 100dvh;
// Output: height: 100vh;   (original removed)
preserve: false

Applies to at-rules too: @media (min-height: 100dvh) is replaced by its vh fallback block.

Note: replace: true still works as a deprecated alias for preserve: false.

includeCustomProps

Transform custom properties, e.g.:

--header-height: 100dvh;

onlyProperties

Apply transformation only to selected properties. Accepts a string, a RegExp, or an array of both:

onlyProperties: ['height', /^(min|max)-height$/]
onlyProperties: 'height'

excludeProperties

Skip transformation for selected properties. Same format as onlyProperties:

excludeProperties: [/^padding/, 'margin']

At-rules participate in property filters under the names @media, @supports, @container. For example, excludeProperties: ['@media'] skips media query params, and onlyProperties: ['height'] disables at-rule transformation entirely (the allowlist doesn't include @media).

browserslist

Auto-skip the entire plugin if all target browsers (resolved from your browserslist config) support dynamic viewport units. Requires caniuse-api as an optional dependency:

npm install caniuse-api --save-dev
browserslist: true                  // resolve targets from project browserslist config
browserslist: 'last 2 versions'     // or pass a query directly
browserslist: ['chrome >= 120', 'safari >= 17']

If caniuse-api is not installed, the plugin quietly keeps transforming as usual. An invalid query or broken browserslist config, however, throws at plugin init — misconfiguration is never silently ignored.

fastSkip

Skip whole files that contain no viewport units using a single regex scan over the raw source, instead of visiting every declaration (default: false):

fastSkip: true

Warning: do not enable when an earlier plugin in the same PostCSS pipeline generates viewport units that are not present in the source file — e.g. Tailwind producing h-dvh utilities from @tailwind utilities. The scan only sees the original source.

debug

Controls logging output:

  • false — no output (default)
  • true or 'verbose' — per-transform warnings + summary with timing
  • 'minimal' — summary only (e.g., "3 declarations, 1 at-rules transformed (0.42ms)")

Output is emitted via PostCSS result.warn(), so it integrates with postcss-reporter and other PostCSS tooling.

strict

Throws an error with source position when dynamic viewport units are found. Useful for CI pipelines to enforce that all viewport units have been manually reviewed.

Strict throws exactly where a transform would otherwise happen — declarations and at-rules that are skipped by excludeProperties/onlyProperties, custom properties without includeCustomProps, and idents that merely contain a unit substring (e.g. var(--dvh-color)) do not trigger it.

customUnits

Add custom unit-to-fallback mappings beyond the built-in ones:

customUnits: { cqh: 'vh', cqw: 'vw' }

Custom units are merged with built-in units. You can also override built-in mappings. Chains are allowed (cqh → svh → vh produces progressive fallbacks); mapping cycles (e.g. { dvh: 'svh', svh: 'dvh' }) are rejected at plugin init.

onTransform(meta)

Callback fired for every transformation (declarations and at-rules).

onComplete(stats)

Callback fired after processing with statistics:

onComplete(stats) {
  console.log(stats.declarations); // fallback declarations added
  console.log(stats.atRules);      // at-rule fallbacks added
  console.log(stats.skipped);      // dedup skips
  console.log(stats.timeMs);       // processing time in ms
}

Control Comments

Disable transformation for a specific declaration by placing a comment before it:

.app {
  /* postcss-viewport-fallback: off */
  height: 100dvh; /* will NOT get a fallback */
  width: 50dvw;   /* will get a fallback */
}

Or disable a whole range with disable / enable (applies to all following siblings and cascades into nested blocks):

/* postcss-viewport-fallback: disable */
.hero { height: 100dvh; }                          /* skipped */
@media (min-height: 50dvh) { .x { width: 10dvw; } } /* skipped, including contents */
/* postcss-viewport-fallback: enable */
.app { height: 100dvh; }                           /* gets a fallback */

Supported units

All small (sv*), large (lv*), and dynamic (dv*) viewport units from CSS Values 4:

| Modern units | Fallback | | ------------------- | -------- | | dvw, svw, lvw | vw | | dvh, svh, lvh | vh | | dvi, svi, lvi | vi | | dvb, svb, lvb | vb | | dvmin, svmin, lvmin | vmin | | dvmax, svmax, lvmax | vmax |

Matching is case-insensitive (100DVH100vh), per CSS spec.

Honest note on vi/vb fallbacks: logical viewport units (vi, vb) shipped in browsers at roughly the same time as their dynamic variants (Chromium 108, Safari 15.4) — a browser missing dvi almost certainly also misses vi. The practical value of this plugin is in the vh/vw/vmin/vmax fallbacks; vi/vb mappings are provided for completeness.


Comparison

| | postcss-viewport-fallback | postcss-100vh-fix | postcss-viewport-unit-fallback | LightningCSS | postcss-preset-env | | --- | :---: | :---: | :---: | :---: | :---: | | All 18 sv*/lv*/dv* units | ✅ | ❌ (100vh only) | ⚠️ height units only | ❌ | ❌ | | dvw/width & logical units | ✅ | ❌ | ❌ | ❌ | ⚠️ vi/vb only | | Inside calc()/min()/clamp() | ✅ | ❌ | ❌ | — | — | | @media/@supports/@container | ✅ | ❌ | ❌ | — | — | | Preserves !important | ✅ | ✅ | ❌ (bug) | — | — | | Dynamic runtime fallback option | ✅ (css-var) | ❌ | ❌ | ❌ | ❌ | | browserslist auto-skip | ✅ | ❌ | ❌ | ✅ (targets) | ✅ (stage) | | Actively maintained | ✅ | ⚠️ | ❌ (2023) | ✅ | ✅ |

  • LightningCSS does not lower viewport units and won't — the maintainer's position is that dvw → vw isn't a correct static lowering and a proper polyfill needs JavaScript (which is exactly what strategy: 'css-var' + the runtime provides).
  • Autoprefixer only adds vendor prefixes; units are not prefixable, so it never touches them.
  • postcss-100vh-fix solves the older iOS -webkit-fill-available problem (height-only, breaks in calc()); it's orthogonal, not a dvh fallback.

Gotchas worth knowing

Being the viewport-units tool means being honest about their sharp edges — a fallback plugin can't fix these, but you should know them:

  • dvh doesn't react to the on-screen keyboard. By default the dynamic viewport ignores the virtual keyboard. Opt in with <meta name="viewport" content="interactive-widget=resizes-content"> (Chromium-only today).
  • dvh can jank. Some browsers debounce dynamic viewport updates rather than tracking at 60fps. For sticky/animated elements, svh (smallest viewport) is often the calmer choice than dvh.
  • svh vs dvh: svh assumes browser chrome is visible (safe, never clipped); dvh follows the live viewport (uses all space, but shifts). Pick svh for guaranteed-visible content, dvh for full-bleed.
  • Safari 15.6 (macOS) has a known bug where dvh renders larger than expected (WebKit #242758).

Installation

npm install postcss-viewport-fallback --save-dev

Usage (PostCSS Config)

import viewportFallback from 'postcss-viewport-fallback';

export default {
  plugins: [viewportFallback()],
};

Usage with Frameworks

Tailwind CSS

Tailwind has declined to emit vh fallbacks for its dvh utilities — this plugin is the answer. Order matters: list viewportFallback after Tailwind so it transforms the generated h-dvh / min-h-dvh / max-h-dvh output:

// postcss.config.js — Tailwind v3
export default {
  plugins: {
    tailwindcss: {},
    'postcss-viewport-fallback': {},
  },
};
// Tailwind v4 (@tailwindcss/postcss)
export default {
  plugins: {
    '@tailwindcss/postcss': {},
    'postcss-viewport-fallback': {},
  },
};

Because Tailwind generates utilities during the build (they aren't in your source), leave fastSkip off for the Tailwind entry — the raw-source scan wouldn't see them.

Vue / Nuxt

Works with <style> blocks in .vue files without extra configuration — vue-loader and Vite handle PostCSS natively.

Svelte / SvelteKit

Works with <style> blocks in .svelte files. Vite and svelte-preprocess apply PostCSS automatically.

styled-components / Emotion (CSS-in-JS)

For template literal CSS-in-JS, configure postcss-jsx as a syntax plugin:

import postcssJsx from 'postcss-jsx';

export default {
  plugins: [viewportFallback()],
  syntax: postcssJsx,
};

HTML inline styles

For <style> tags in .html files, use postcss-html:

import postcssHtml from 'postcss-html';

export default {
  plugins: [viewportFallback()],
  syntax: postcssHtml,
};

Development

npm test               # vitest suite
npm run test:coverage  # suite + v8 coverage report
npm run typecheck      # tsc --noEmit
npm run lint           # eslint
npm run build          # tsup → dist (esm + cjs + d.ts/d.cts)
npm run check:package  # publint + arethetypeswrong (package exports health)
npm run bench          # quick perf benchmark on 20k generated rules

CI runs lint, typecheck, tests (Node 20/22/24), build, and package checks on every push/PR. Coverage is reported on the Node 22 job.

Releases

Fully automated with semantic-release: every push to main with Conventional Commits (fix: → patch, feat: → minor, BREAKING CHANGE → major) runs CI and, if a release is due, bumps the version, updates CHANGELOG.md, tags a GitHub release, and publishes to npm with provenance — no manual steps.


License

MIT License © 2025 Maksim Kravtsov