Viewport Units (vw, vh, svh, dvh)
Viewport units are relative to the browser's viewport — the visible area of the web page. They're essential for creating full-screen layouts, hero sections, and fluid typography without JavaScript. Modern CSS added several variants to fix a long-standing mobile browser problem: the jumping address bar.
The viewport unit families
Unit | Equal to | Notes |
|---|---|---|
| 1% of the viewport width | Includes scrollbar width on some browsers |
| 1% of the viewport height | On mobile: full height including browser UI — causes the "100vh problem" |
| 1% of the smaller dimension (width or height) | Useful for square elements |
| 1% of the larger dimension | Less commonly used |
| 1% of the small viewport height (UI shown) | Stable — matches viewport with address bar visible |
| 1% of the large viewport height (UI hidden) | Stable — matches viewport with address bar hidden |
| 1% of the dynamic viewport height | Updates as browser UI shows/hides |
| Same concept but for width | Less commonly needed |
| 1% of a container's dimensions | Container query units — covered in modern CSS |
The basic units — vw and vh
/* Full-viewport hero section */
.hero {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* Fluid font size based on viewport width */
h1 {
font-size: 5vw; /* 5% of viewport width — scales with screen size */
}
/* Better: clamp() to set min and max bounds */
h1 {
font-size: clamp(1.75rem, 5vw, 3.5rem);
}
/* Centering a fixed overlay */
.modal {
position: fixed;
width: 90vw;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}The 100vh problem on mobile
On mobile browsers (especially iOS Safari), 100vh is calculated including the browser's address bar and navigation controls — the full height of the viewport when those UI elements are hidden. When the page first loads and the address bar is visible, 100vh is taller than the actual visible area, causing overflow.
Mobile browser with address bar visible: Visible area: ~660px tall 100vh: ~740px tall (the full viewport without UI) Problem: the "full screen" section actually overflows! Mobile browser after scrolling (address bar hides): Visible area: ~740px tall 100vh: ~740px tall (now matches) But the layout already calculated wrong on load.
/* The fix — use svh or dvh instead of vh for full-screen sections */
/* svh (small viewport height) — always the smaller value, address bar visible */
.hero {
min-height: 100svh; /* safe — never taller than visible area */
}
/* dvh (dynamic viewport height) — updates as browser UI appears/disappears */
.hero {
min-height: 100dvh; /* best for sticky navigation effects */
}
/* Fallback for older browsers that don't support svh/dvh */
.hero {
min-height: 100vh; /* fallback */
min-height: 100svh; /* overrides in browsers that support it */
}vw for full-bleed layouts inside padded containers
A classic use of vw is breaking an element out of a padded container to span the full viewport width:
/* Container has 24px padding on each side */
.container {
max-width: 1200px;
padding-inline: 24px;
margin-inline: auto;
}
/* Make a child span the full viewport width */
.full-bleed {
width: 100vw;
margin-inline: calc(50% - 50vw);
/* Explanation: 50% = half container width; 50vw = half viewport */
/* The difference between them = the margin needed to reach viewport edge */
}
/* Modern alternative using negative margin */
.full-bleed {
margin-inline: -24px; /* if container padding is known */
}vmin and vmax
/* vmin — useful for square elements that fit in any orientation */
.square-logo {
width: 40vmin; /* 40% of the smaller dimension */
height: 40vmin; /* always square, always fits whether portrait or landscape */
}
/* Fluid typography with vmin — consistent size change on orientation flip */
h1 {
font-size: clamp(1.5rem, 6vmin, 4rem);
}
/* vmax — fills the larger dimension */
.background-pattern {
width: 200vmax; /* always larger than the viewport in any orientation */
height: 200vmax;
}100vw and scrollbar width
On operating systems where scrollbars take up space (Windows, older macOS), 100vw includes the scrollbar width. This can cause unwanted horizontal scrollbars when applied to elements that are already full-width:
/* Problem: on Windows, 100vw may be slightly wider than the layout width */
.full-width {
width: 100vw; /* may cause horizontal scroll on Windows */
}
/* Solution: use width: 100% instead when you want "full container width" */
.full-width {
width: 100%; /* relative to the containing block — doesn't include scrollbar */
}
/* Or use 100vw only for fixed/absolute positioned elements */
.fixed-banner {
position: fixed;
width: 100vw; /* fine here — fixed positioning is relative to the viewport */
}