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);
}
Viewport Units: Sizing Relative to the Screen
Viewport units size elements as a percentage of the browser window, not the parent. 1vw = 1% of viewport width, 1vh = 1% of height. Great for hero sections, full-screen panels, and fluid typography.
| Unit | Equals |
|---|---|
vw | 1% of viewport width |
vh | 1% of viewport height |
vmin | 1% of the smaller side |
vmax | 1% of the larger side |
The famous 100vh mobile bug
height: 100vh on mobile is taller than the visible area — 100vh ignores the browser's address bar, so content hides behind it and you get scroll. The modern fix is the dynamic viewport unit dvh:
.hero { min-height: 100vh; } /* old: overshoots on mobile */
.hero { min-height: 100dvh; } /* new: excludes browser chrome, fits */
Fluid type without media queries: combine with clamp() — font-size: clamp(1rem, 2.5vw, 2rem) scales with the screen but never below 1rem or above 2rem. Use vmin for elements that must stay square on any orientation.
🏋️ Practical Exercise
- Size an element with
vwandvh. - Make a full-height hero with
height: 100vh. - Use
vminandvmax. - Combine viewport units with
clamp(). - Compare
%againstvwsizing.
🔥 Challenge Exercise
Build a full-viewport-height landing hero (100vh) with a heading sized fluidly using clamp() and vw so it scales with the viewport but stays within min/max bounds. Explain the difference between vw, vh, vmin, vmax, and %.
📋 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.
Interview Questions
- What are viewport units in CSS?
- What is the difference between
vwand%? - What do
vminandvmaxrepresent? - How does
clamp()create fluid typography? - What is a common pitfall with
100vhon mobile?
Related Topics
FAQ
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.

