Fastest way to measure the real visible mobile viewport without 100vh glitches, resize/scroll jitter, or keyboard hacks.
Stop guessing mobile viewport sizes. viewport-truth delivers stable, keyboard-aware visible viewport metrics (VisualViewport-first) across iOS Safari, Android Chrome, PWAs, and webviews—framework adapters included, SSR-safe, zero runtime deps.
npm i viewport-truth
# or: yarn add viewport-truth
# or: pnpm add viewport-truth
Minimal flow: import → create → subscribe → get { width, height, isKeyboardOpen, isStable }.
import { createViewportTruth } from "viewport-truth/vanilla";
const vt = createViewportTruth();
const unsub = vt.subscribe((v) => {
if (!v) return;
console.log(
`visible=${v.width}x${v.height} keyboard=${v.isKeyboardOpen} stable=${v.isStable}`
);
});
// later:
// unsub();
// vt.destroy();
A tiny bottom bar that stays visible and shows exactly how much viewport you lost.
Run it in a real page (Vite/Parcel/Next) — the snippet won’t execute inside README.
On mobile: scroll a bit (URL bar), then focus the input (keyboard).
<div id="app" style="padding:16px 16px 96px">
<input
placeholder="Focus me to open keyboard"
style="width:100%;padding:12px;font-size:16px;box-sizing:border-box"
/>
<p style="margin:12px 0 0;color:#444">
Tip: scroll a bit (URL bar animates), then focus the input.
</p>
<div style="height:120vh"></div>
<div id="bar"></div>
</div>
<script type="module">
import { createViewportTruth } from "viewport-truth/vanilla";
const bar = document.getElementById("bar");
Object.assign(bar.style, {
position: "fixed",
left: "0",
right: "0",
bottom: "0",
padding: "10px 12px",
font: "12px/1.35 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
background: "rgba(0,0,0,.86)",
color: "white",
zIndex: "9999",
whiteSpace: "pre",
});
const vt = createViewportTruth();
vt.subscribe((v) => {
if (!v) return;
// Fallback keeps the snippet working even if layoutWidth/layoutHeight aren't present.
const layoutW = v.layoutWidth ?? v.width;
const layoutH = v.layoutHeight ?? v.height;
const lost = Math.max(0, layoutH - v.height);
bar.textContent =
`visible: ${v.width}×${v.height}
layout: ${layoutW}×${layoutH}
lost: ${lost}px
keyboard: ${v.isKeyboardOpen}
stable: ${v.isStable}`;
});
</script>
A few concrete, technical reasons it behaves well on mobile:
isStable flips after 150ms (default) without geometry changes.Core snapshot fields you’ll typically use:
width, height — visible viewport size (CSS px)layoutWidth, layoutHeight — layout viewport (basis for keyboard detection)isKeyboardOpen — geometry-based keyboard inferenceisStable — “animations settled” signal for UI decisionsVanilla store:
createViewportTruth() from viewport-truth/vanilla → creates a store with subscribe() and destroy()Framework adapters:
useViewportTruth from viewport-truth/reactuseViewportTruth from viewport-truth/vueviewportTruth from viewport-truth/sveltecreateViewportTruth from viewport-truth/solidViewportTruthDirective from viewport-truth/angularFull types and signatures: see dist/*.d.ts (or TypeScript IntelliSense).
Adapter Docs: React • Vue • Svelte • Solid • Angular
Tip: Open links in a new tab with Ctrl+Click (Windows/Linux) or Cmd+Click (macOS).
FAQ • Common pitfalls • Smoke test (clean environment) • Versioning policy
“We ate the Geometry Hell for you: jumping
100vh, jitteryresize, modals under the keyboard.
You saved hours (and sanity). A donation is a fair trade for a rock-solid UI and weekends free from debugging.”
If this library saved you time, please consider supporting the development:
MIT