viewportify
v1.0.0
Published
π₯οΈ The Ultimate Cross-Platform Viewport Toolkit - Consistent viewport dimensions across iOS, Android, Windows & all browsers. Solves 100vh issues, supports svh/lvh/dvh, keyboard detection, safe areas & more!
Maintainers
Readme
π₯οΈ Viewportify
The Ultimate Cross-Platform Viewport Toolkit
Stop fighting with window.innerHeight on iOS! Viewportify provides consistent, accurate viewport dimensions across all platforms and browsersβincluding the infamous iOS Safari toolbar issue.
β¨ Why Viewportify?
| Problem | Viewportify Solution |
|---------|---------------------|
| 100vh is broken on iOS Safari | β
Accurate 100vh value that accounts for toolbar |
| window.innerHeight changes on scroll | β
Stable svh, lvh, dvh measurements |
| No native svh/lvh/dvh support | β
JavaScript polyfill with CSS variables |
| Keyboard detection on mobile | β
Built-in keyboard visibility tracking |
| Safe area insets for notches | β
Easy access to safe area values |
| Responsive breakpoints | β
Tailwind-style breakpoint system |
| React integration | β
Ready-to-use hooks |
| Need width/height on resize | β
useWindowSize() hook updates on resize |
| Measure element by ref | β
useElementSize(ref) returns exact dimensions |
| Non-React resize listener | β
onWindowResize() with debounce support |
π Quick Start
npm install viewportifyimport { getHeight, getWidth, isMobile, isIOS } from 'viewportify';
// Get accurate viewport dimensions (works on iOS!)
console.log(getWidth()); // 390
console.log(getHeight()); // 844 (correct even with Safari toolbar!)
// Device detection
console.log(isMobile()); // true
console.log(isIOS()); // trueπ¦ Installation
# npm
npm install viewportify
# yarn
yarn add viewportify
# pnpm
pnpm add viewportifyCDN (UMD)
<script src="https://unpkg.com/viewportify/dist/index.umd.min.js"></script>
<script>
const { getHeight, isMobile } = Viewportify;
console.log(getHeight());
</script>π API Reference
Core Viewport Functions
import {
getWidth, // Viewport width in pixels
getHeight, // Viewport height (accurate for iOS!)
getInnerWidth, // window.innerWidth
getInnerHeight, // window.innerHeight
get100vh, // Accurate 100vh value
getSvh, // Small viewport height (toolbar expanded)
getLvh, // Large viewport height (toolbar collapsed)
getDvh, // Dynamic viewport height
getDPR, // Device pixel ratio
getViewport, // Full ViewportInfo object
} from 'viewportify';Device Detection
import {
isMobile, // true for phones
isTablet, // true for tablets
isDesktop, // true for desktops
isIOS, // true for iPhone/iPad
isAndroid, // true for Android devices
isSafari, // true for Safari browser
isTouch, // true if touch-capable
isStandalone, // true if running as PWA
} from 'viewportify';Orientation & Keyboard
import {
getOrientation, // 'portrait' | 'landscape'
isKeyboardVisible, // true when keyboard is open (mobile)
getKeyboardHeight, // Estimated keyboard height in pixels
} from 'viewportify';Safe Area & Scrollbar
import { getSafeArea, getScrollbarWidth } from 'viewportify';
const safeArea = getSafeArea();
// { top: 47, right: 0, bottom: 34, left: 0 }
const scrollbar = getScrollbarWidth();
// 17 (Windows), 0 (macOS with overlay scrollbars)Breakpoints
import {
getCurrentBreakpoint,
matchesBreakpoint,
} from 'viewportify';
// Tailwind-style breakpoints: xs, sm, md, lg, xl, 2xl
console.log(getCurrentBreakpoint()); // 'lg'
console.log(matchesBreakpoint('md')); // true if >= 768pxFull Instance API
import { Viewportify } from 'viewportify';
const vp = new Viewportify({
debounceTime: 100, // Resize event debounce (ms)
setCSSVariables: true, // Auto-set CSS custom properties
cssPrefix: '--vp', // CSS variable prefix
trackKeyboard: true, // Track keyboard visibility
iosVhFix: true, // Apply iOS 100vh fix
breakpoints: { // Custom breakpoints
xs: 0,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1536,
},
onChange: (info) => { // Viewport change callback
console.log('Viewport changed:', info);
},
});
// Get info
console.log(vp.info);
// Subscribe to changes
const unsubscribe = vp.subscribe((info) => {
console.log('New dimensions:', info.width, info.height);
});
// Breakpoint utilities
console.log(vp.getCurrentBreakpoint()); // 'lg'
console.log(vp.isAbove('md')); // true
console.log(vp.isBelow('xl')); // true
console.log(vp.isBetween('sm', 'lg')); // true
// Media query tracking
const darkMode = vp.matchMedia('(prefers-color-scheme: dark)');
console.log(darkMode.matches);
darkMode.subscribe(({ matches }) => {
console.log('Dark mode:', matches);
});
// Cleanup
unsubscribe();
vp.destroy();βοΈ React Hooks
Basic Hooks
import {
useViewport,
useBreakpoint,
useMediaQuery,
useIsMobile,
useOrientation,
useKeyboard,
useWindowSize, // NEW: Simple width/height object
useViewportSize, // NEW: iOS-accurate width/height
useElementSize, // NEW: Measure any element by ref
} from 'viewportify';
function MyComponent() {
// Full viewport info (re-renders on change)
const viewport = useViewport();
// Current breakpoint
const breakpoint = useBreakpoint(); // 'sm' | 'md' | 'lg' | etc.
// Media query
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
// Simple device check
const isMobile = useIsMobile();
// Orientation
const orientation = useOrientation(); // 'portrait' | 'landscape'
// Keyboard state (mobile)
const { visible: keyboardVisible, height: keyboardHeight } = useKeyboard();
return (
<div style={{ height: viewport.height }}>
<p>Screen: {viewport.width} x {viewport.height}</p>
<p>Breakpoint: {breakpoint}</p>
<p>Mobile: {isMobile ? 'Yes' : 'No'}</p>
<p>Orientation: {orientation}</p>
{keyboardVisible && (
<p>Keyboard height: {keyboardHeight}px</p>
)}
</div>
);
}useWindowSize - Window Dimensions Hook
Returns { width, height } that automatically updates on window resize.
import { useWindowSize } from 'viewportify';
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
<p>Window: {width} x {height}</p>
{width < 768 && <MobileNav />}
{width >= 768 && <DesktopNav />}
</div>
);
}useViewportSize - iOS-Accurate Dimensions Hook
Same as useWindowSize but returns iOS-accurate height (handles Safari toolbar issue).
import { useViewportSize } from 'viewportify';
function FullscreenHero() {
const { width, height } = useViewportSize();
// This height works correctly on iOS Safari!
return (
<div style={{ width, height, background: 'linear-gradient(...)' }}>
<h1>Full Screen Hero</h1>
</div>
);
}useElementSize - Measure Any Element by Ref
Pass a ref to any element and get its exact dimensions, updating automatically on resize.
import { useRef } from 'react';
import { useElementSize } from 'viewportify';
function MeasuredComponent() {
const containerRef = useRef<HTMLDivElement>(null);
const size = useElementSize(containerRef);
return (
<div ref={containerRef} style={{ width: '50%', padding: 20 }}>
<p>This container is:</p>
<p>{size.width}px wide Γ {size.height}px tall</p>
<p>Position: ({size.x}, {size.y})</p>
</div>
);
}Returns:
{
width: number; // Element width
height: number; // Element height
top: number; // Distance from viewport top
left: number; // Distance from viewport left
right: number; // Distance from viewport right edge
bottom: number; // Distance from viewport bottom edge
x: number; // Same as left
y: number; // Same as top
}π Window Resize Support (Non-React)
onWindowResize - Subscribe to Resize Events
import { onWindowResize } from 'viewportify';
// Subscribe to resize events
const unsubscribe = onWindowResize(({ width, height }) => {
console.log(`Window resized: ${width} x ${height}`);
}, {
debounce: 100, // Optional: debounce in ms
immediate: true, // Optional: call immediately with current size
});
// Later: cleanup
unsubscribe();observeElementSize - Watch Element Size Changes
import { observeElementSize } from 'viewportify';
const element = document.getElementById('my-element');
const observer = observeElementSize(element, (size) => {
console.log('Element size:', size.width, size.height);
console.log('Position:', size.x, size.y);
});
// Get current size anytime
console.log(observer.getSize());
// Later: cleanup
observer.disconnect();getElementSize - One-time Element Measurement
import { getElementSize } from 'viewportify';
const element = document.getElementById('my-element');
const size = getElementSize(element);
console.log(size); // { width, height, top, left, right, bottom, x, y }
## π¨ CSS Variables
When `setCSSVariables` is enabled (default), Viewportify sets these CSS custom properties on `:root`:
```css
:root {
/* Basic dimensions */
--vp-width: 1920px;
--vp-height: 1080px;
--vp-vh: 10.8px; /* 1vh in pixels */
--vp-vw: 19.2px; /* 1vw in pixels */
--vp-vmin: 10.8px;
--vp-vmax: 19.2px;
/* Modern viewport units (polyfilled) */
--vp-svh: 900px; /* Small viewport height */
--vp-lvh: 1080px; /* Large viewport height */
--vp-dvh: 1020px; /* Dynamic viewport height */
--vp-svw: 1920px;
--vp-lvw: 1920px;
--vp-dvw: 1920px;
/* Safe area insets */
--vp-safe-top: 47px;
--vp-safe-right: 0px;
--vp-safe-bottom: 34px;
--vp-safe-left: 0px;
/* Extras */
--vp-scrollbar: 17px;
--vp-dpr: 2;
--vp-keyboard-height: 0px;
/* iOS 100vh fix */
--vh: 10.8px; /* 1vh accurate for iOS */
}Using CSS Variables
/* Full-height section that works on iOS */
.hero {
height: calc(var(--vh, 1vh) * 100);
/* Or use the direct value */
height: var(--vp-height);
}
/* Respect safe areas on notched devices */
.content {
padding-top: var(--vp-safe-top);
padding-bottom: var(--vp-safe-bottom);
}
/* Full viewport width minus scrollbar */
.full-width {
width: calc(100vw - var(--vp-scrollbar));
}
/* Adjust for keyboard on mobile */
.chat-input {
bottom: var(--vp-keyboard-height);
transition: bottom 0.3s ease;
}π ViewportInfo Object
The full viewport info object contains:
interface ViewportInfo {
// Basic dimensions
width: number; // Viewport width
height: number; // Viewport height (iOS-accurate)
vh: number; // 1vh in pixels
vw: number; // 1vw in pixels
vmin: number; // min(vh, vw)
vmax: number; // max(vh, vw)
// Modern viewport units
svh: number; // Small viewport height
lvh: number; // Large viewport height
dvh: number; // Dynamic viewport height
svw: number; // Small viewport width
lvw: number; // Large viewport width
dvw: number; // Dynamic viewport width
// Screen info
screenWidth: number; // screen.width
screenHeight: number; // screen.height
dpr: number; // devicePixelRatio
orientation: 'portrait' | 'landscape';
// Device detection
isTouch: boolean;
isMobile: boolean;
isTablet: boolean;
isDesktop: boolean;
isIOS: boolean;
isAndroid: boolean;
isSafari: boolean;
isStandalone: boolean; // PWA mode
// Safe area
safeArea: {
top: number;
right: number;
bottom: number;
left: number;
};
// Scrollbar
scrollbarWidth: number;
// Keyboard (mobile)
isKeyboardVisible: boolean;
keyboardHeight: number;
}π§ Understanding iOS Viewport Issues
On iOS Safari, window.innerHeight and 100vh behave unexpectedly:
- Initial load: Includes the Safari toolbar in the measurement
- After scroll: Toolbar collapses, but
100vhdoesn't update - Result: Elements set to
100vhoverflow the visible viewport
Viewportify solves this by using multiple measurement techniques:
- CSS
100vhelement injection for accurate measurement visualViewportAPI for dynamic tracking- New CSS units (
svh,lvh,dvh) when available - Automatic CSS variable updates
π± Platform Support
| Platform | Support | |----------|---------| | Chrome (Desktop) | β Full | | Firefox (Desktop) | β Full | | Safari (Desktop) | β Full | | Edge (Desktop) | β Full | | Chrome (Android) | β Full | | Safari (iOS) | β Full (with fixes) | | Firefox (Android) | β Full | | Samsung Internet | β Full | | PWA (All) | β Full | | SSR (Node.js) | β Safe (returns zeros) |
π€ Comparison with Alternatives
| Feature | Viewportify | ios-inner-height | viewport-dimensions | |---------|-------------|------------------|---------------------| | iOS Safari fix | β | β | β | | svh/lvh/dvh | β | β | β | | CSS variables | β | β | β | | React hooks | β | β | β | | useWindowSize | β | β | β | | useElementSize (ref) | β | β | β | | Resize subscription | β | β | β | | Keyboard detection | β | β | β | | Safe area insets | β | β | β | | Breakpoints | β | β | β | | TypeScript | β | β | β | | Tree-shakable | β | N/A | β | | Last updated | 2025 | 2018 | 2014 | | Bundle size | ~5kb | ~1kb | ~2kb |
π License
MIT Β© Viewportify Contributors
Made with β€οΈ for developers tired of viewport inconsistencies
β Star us on GitHub if this saved you time!
