The Classic Viewport Units
/* vw — viewport width */
/* 1vw = 1% of viewport width */
.full-width { width: 100vw; } /* always the full viewport width */
.half-width { width: 50vw; }
h1 { font-size: 5vw; } /* scales with viewport width */
/* vh — viewport height */
/* 1vh = 1% of viewport height */
.full-height { height: 100vh; } /* full screen height */
.hero { min-height: 100vh; } /* at least full screen tall */
.half-screen { height: 50vh; }
/* vmin — 1% of the SMALLER viewport dimension */
/* On portrait phone (360×800): 1vmin = 3.6px */
/* On landscape (800×360): 1vmin = 3.6px */
.square { width: 50vmin; height: 50vmin; } /* always fits screen */
/* vmax — 1% of the LARGER viewport dimension */
/* Useful for elements that should always be prominent regardless of orientation */
.overlay { font-size: 8vmax; }
The 100vh Mobile Problem
On mobile browsers, 100vh is calculated as if the browser chrome (address bar, navigation bar) doesn't exist. When the address bar is visible, 100vh is taller than the actual visible area — causing overflow.
Mobile browsers hide/show the address bar as you scroll. Chrome on Android and Safari on iOS calculate 100vh as the maximum viewport height (address bar hidden). When the address bar is visible, the page is taller than the screen. This causes "above the fold" content to actually be below the fold — a classic mobile layout bug.
/* PROBLEM: overflows when mobile address bar is visible */
.hero { height: 100vh; }
/* LEGACY WORKAROUND: JavaScript fixes the real viewport height */
/* In JS: document.documentElement.style.setProperty('--vh', window.innerHeight * 0.01 + 'px'); */
/* In CSS: */
.hero { height: calc(var(--vh, 1vh) * 100); }
/* SIMPLE WORKAROUND: use min-height instead of height */
.hero { min-height: 100vh; } /* still works, slightly different behavior */
Modern Viewport Units (2022+)
CSS now has three new families of viewport units specifically designed to solve the mobile browser chrome problem. Supported in all modern browsers since 2022–2023.
/* ─── Small Viewport (svh, svw, svmin, svmax) ───
Assumes the largest possible browser chrome (address bar visible).
Always fits in the visible area — may feel smaller than expected.
Use when: you need the element to ALWAYS be fully visible */
.modal { height: 100svh; } /* never clipped by browser chrome */
.banner { height: 20svh; }
/* ─── Large Viewport (lvh, lvw, lvmin, lvmax) ───
Assumes no browser chrome (address bar hidden, maximum space).
Same as old 100vh — can overflow when chrome is visible.
Use when: you're okay with overflow or scrolling */
.hero-section { min-height: 100lvh; }
/* ─── Dynamic Viewport (dvh, dvw, dvmin, dvmax) ───
Updates in real time as browser chrome shows/hides.
Causes reflow on scroll (a potential performance concern).
Use when: you want to perfectly fill the visible screen at all times */
.fullscreen-app { height: 100dvh; }
.sticky-footer { bottom: env(safe-area-inset-bottom); }
/* ─── Practical recommendation ─── */
/* For hero sections */
.hero { min-height: 100svh; } /* safe: always fits */
/* For full-screen overlays/modals */
.modal { height: 100dvh; } /* accurate: updates with chrome */
/* For app-shell layouts */
.app { height: 100dvh; } /* fills current visible area */
Viewport Units for Fluid Typography
Viewport units scale text with the browser window — useful for large display headings:
/* Simple fluid heading — scales with viewport width */
h1 { font-size: 5vw; }
/* Problem: too small on mobile (5vw of 375px = 18.75px), too large on 4K */
/* Better: clamp() with viewport unit in the middle */
h1 { font-size: clamp(2rem, 5vw, 4rem); }
/* min preferred max
min-width mobile: 2rem (32px)
scales up with viewport using 5vw
caps at 4rem (64px) on large screens */
/* Full type scale with clamp */
h1 { font-size: clamp(2rem, 5vw + 1rem, 4rem); }
h2 { font-size: clamp(1.5rem, 3vw + 1rem, 2.5rem); }
h3 { font-size: clamp(1.2rem, 2vw + 1rem, 2rem); }
p { font-size: clamp(1rem, 1.5vw, 1.25rem); }
/* The + 1rem part prevents the minimum from being too small on very narrow screens */
Practical Use Cases
/* 1. Full-screen hero section */
.hero {
min-height: 100svh;
display: flex;
align-items: center;
justify-content: center;
}
/* 2. Sticky full-screen sidebar on desktop */
.sidebar {
position: sticky;
top: 0;
height: 100svh;
overflow-y: auto;
}
/* 3. Modal that fits the screen */
.modal {
max-height: 90svh; /* leave 10% gap */
overflow-y: auto;
}
/* 4. Full-width breakout image in article */
.full-bleed {
width: 100vw;
margin-left: calc(-50vw + 50%); /* center-aligned full-bleed */
}
/* 5. Safe area insets (iPhone notch/home indicator) */
.sticky-nav {
padding-bottom: env(safe-area-inset-bottom);
}
/* 6. Fluid gap/spacing */
.section {
padding: clamp(40px, 8vw, 120px) clamp(16px, 5vw, 80px);
}
/* 7. Responsive container without media queries */
.container {
width: min(100% - 2rem, 1200px); /* min() chooses the smaller value */
margin: 0 auto;
}
Safe Area Insets – Handling Notches
/* Add to <meta viewport> to enable safe area support */
/* <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> */
/* Use env() to access safe area values */
.bottom-bar {
padding-bottom: env(safe-area-inset-bottom); /* home indicator on iPhone */
}
.top-bar {
padding-top: env(safe-area-inset-top); /* notch/dynamic island */
}
/* Combined with regular padding */
.nav {
padding: 16px;
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
padding-bottom: max(16px, env(safe-area-inset-bottom));
}
/* Or use padding shorthand with env() fallback */
.safe-wrapper {
padding:
env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left);
}
📋 Summary
- vw — 1% of viewport width.
100vw= full window width. - vh — 1% of viewport height.
100vhbuggy on mobile browsers. - vmin / vmax — 1% of smaller / larger dimension. Useful for squares that fit any orientation.
- svh — small viewport height (chrome visible). Safe, never overflows. Use for most things.
- dvh — dynamic viewport height (updates as chrome shows/hides). Most accurate, reflows on scroll.
- lvh — large viewport height (chrome hidden). Same as old vh behavior.
- clamp(min, vw-value, max) — fluid typography that scales without media queries.
- env(safe-area-inset-*) — padding for iPhone notch/home indicator.
- min(100% - 2rem, 1200px) — responsive container without media queries.
Frequently Asked Questions
For hero sections: min-height: 100svh — safe, always fits in the visible area without overflow. For full-screen app layouts (no scroll, like a native app): height: 100dvh — accurately tracks the visible area. For background sections where some overflow is acceptable: min-height: 100vh still works. Avoid lvh for anything that must fit — it's the same as the old vh problem.
100vw includes the width of the scrollbar (if visible). On Windows, scrollbars are ~17px wide and take space outside the content area. So 100vw on a page with a vertical scrollbar = content width + 17px, causing horizontal overflow. Fix: use width: 100% instead of 100vw for element width. Reserve 100vw for full-bleed effects using negative margins or translate.
clamp(min, preferred, max) chooses the preferred value, clamped between min and max. Example: font-size: clamp(1rem, 4vw, 3rem) — normally uses 4vw, but never drops below 1rem and never exceeds 3rem. On a 375px screen: 4vw = 15px (above 1rem minimum, so uses 15px). On a 1200px screen: 4vw = 48px (hits max of 3rem = 48px). It's fluid scaling with guardrails.
Safe area insets are the regions of the screen obscured by hardware notches, rounded corners, and home indicators on iPhones and some Android devices. You need them when: (1) building a fullscreen experience with viewport-fit=cover in the viewport meta tag, (2) creating a fixed bottom navigation bar, or (3) a fixed top bar. Without accounting for them, your content may be hidden behind the notch or home indicator. Use env(safe-area-inset-bottom) etc. to add the correct padding.