CSS Viewport Units (vw, vh, vmin, vmax)
Viewport units let you size and space elements as a percentage of the browser window itself, rather than relative to a parent element or a fixed pixel value. Instead of guessing pixel values for every screen size, you tell the browser “make this half the screen’s width” or “make this exactly as tall as the visible window,” and the browser recalculates it live as the window or device orientation changes. This makes viewport units the backbone of full-screen hero sections, fluid typography, and layouts that need to scale smoothly across phones, tablets, and desktops.
There are four core viewport units — vw, vh, vmin, and vmax — plus a newer family (dvh, svh, lvh) that fixes a well-known mobile bug. This lesson covers all of them, how the rendering engine computes them, where they go wrong, and how to use them well.
Overview / How it works
The viewport is the visible area of the browser window in which your page is rendered — it excludes browser chrome like the address bar and bookmarks bar on desktop, though on mobile the definition gets more nuanced (more on that below). Every viewport unit is a percentage of one dimension of that viewport:
| Unit | Equal to |
|---|---|
1vw |
1% of the viewport’s width |
1vh |
1% of the viewport’s height |
1vmin |
1% of whichever is smaller: viewport width or height |
1vmax |
1% of whichever is larger: viewport width or height |
So on a browser window that is 1200px wide and 800px tall: 100vw = 1200px, 100vh = 800px, 100vmin = 800px (the smaller dimension), and 100vmax = 1200px (the larger dimension). Rotate a phone from portrait to landscape and the width/height figures swap, but vmin/vmax automatically track “the short side” and “the long side” regardless of orientation — which is exactly why they’re useful for things that must always fit on screen, like a modal or a circular avatar.
Unlike percentages (%), which resolve against the size of a containing block, viewport units always resolve against the root viewport, no matter how deeply an element is nested. That means a <span> three levels deep inside a small sidebar can still be told font-size: 5vw and it will size itself relative to the entire browser window, not the sidebar. This is powerful, but it’s also why unclamped viewport units are a common source of layout bugs — an element can size itself completely independent of its parent’s actual space, and overflow or become illegible at extreme window sizes.
During layout, the rendering engine treats viewport units as absolute lengths once it knows the viewport size — they are resolved very early, before most percentage-based and content-based sizing, and they participate in the box model exactly like any other length: they can set width, height, padding, margin, font-size, gap, or any other length-accepting property.
Syntax
selector {
property: <number>vw | vh | vmin | vmax;
}
- <number> — any number, including decimals (e.g.
2.5vw) and, in modern CSS, negative numbers are technically valid syntax though rarely useful for sizing. - vw — percentage of viewport width.
- vh — percentage of viewport height.
- vmin — percentage of the smaller of the two viewport dimensions.
- vmax — percentage of the larger of the two viewport dimensions.
Viewport units can be freely mixed with other units inside math functions like calc(), min(), max(), and clamp() — for example calc(100vh - 4rem) or clamp(1rem, 4vw, 2rem). This combination is the single most useful pattern in the unit’s toolkit, because it lets you scale with the viewport while still enforcing a hard floor and ceiling.
Examples
Example 1: A full-viewport hero section
.hero {
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e3a8a, #3b82f6);
color: #ffffff;
text-align: center;
padding: 2rem;
}
.hero h1 {
font-size: 5vw;
margin: 0;
}
Result: The <section class="hero"> fills the entire browser window edge-to-edge with a blue diagonal gradient, and its heading text sits centered both horizontally and vertically. Resize the browser wider and the heading’s font grows proportionally; resize it narrower and the text shrinks — because both the box and the type are locked to the viewport, not to any parent container.
Example 2: Fluid typography with vw and clamp()
.article-title {
font-size: clamp(1.5rem, 4vw + 1rem, 3.5rem);
line-height: 1.2;
max-width: 40ch;
margin: 0 auto 1rem;
}
.article-body {
font-size: clamp(1rem, 0.5vw + 0.9rem, 1.125rem);
line-height: 1.6;
}
Result: The article title renders at a comfortable 1.5rem on very narrow phones, grows smoothly as the window widens, and stops growing once it reaches 3.5rem on large desktop monitors — it never gets small enough to be unreadable or so large it dwarfs the layout. The body text follows the same idea with a much gentler scaling curve, since paragraph text shouldn’t grow nearly as aggressively as a headline.
Example 3: vmin and vmax for orientation-safe sizing
.badge {
width: 30vmin;
height: 30vmin;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #fbbf24, #d97706);
display: flex;
align-items: center;
justify-content: center;
color: #1f2937;
font-weight: bold;
font-size: 4vmin;
}
.fullscreen-panel {
width: 90vmax;
max-width: 100%;
padding: 2vmin;
border: 1px solid #d1d5db;
}
Result: The .badge renders as a perfect circle sized to 30% of whichever viewport dimension is smaller, with a golden radial gradient and bold centered text — because it’s based on vmin, it stays a fully visible circle whether the phone is held upright or sideways, never clipping off-screen. The .fullscreen-panel uses vmax instead, so its width tracks the larger of the two dimensions (usually the width on a normal monitor), while max-width: 100% keeps it from ever overflowing its own container.
How it works step by step
- The browser first determines the initial containing block — the viewport’s width and height in CSS pixels, including scrollbars in most browsers’ calculations.
- Any declaration using
vw,vh,vmin, orvmaxis resolved against that viewport size: the engine multiplies the number by 1% of the relevant dimension (or the min/max of both, forvmin/vmax). - The resolved pixel value is then treated exactly like any other absolute length in the box model — it factors into content box size, then padding, border, and margin are added per the standard box-sizing rules.
- If the value is wrapped in
calc(),clamp(), ormin()/max(), the viewport-derived length is combined with the other operands (rems, pixels, percentages) using normal arithmetic before being applied. - Whenever the viewport itself changes size — the window is resized, the device rotates, or (on mobile) the browser’s UI chrome shows or hides — the browser triggers a fresh layout pass and every viewport-unit value is recalculated from scratch.
That final step is important: viewport units are not static snapshots taken once at page load. They are live, and re-triggering layout on every resize event has a real (if usually small) performance cost, which is why extremely large numbers of elements sized purely in viewport units on a page that resizes often (like a resizable panel) can occasionally show jank on lower-powered devices.
Common Mistakes
Mistake 1: 100vh cuts off content on mobile
On mobile Safari and many mobile Chrome builds, 100vh is calculated against the largest possible viewport — the height available once the address bar and toolbar auto-hide. But when those bars are visible (as they are when the page first loads), the actual visible area is shorter than 100vh, so the bottom of a full-height section gets pushed off-screen or behind the browser UI.
.mobile-hero {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
This isn’t invalid CSS — it’s a real, common layout bug. The fix is to use the newer dynamic viewport height unit, dvh, which the browser recalculates as its UI chrome shows and hides, falling back to vh for older browsers that don’t support it yet:
.mobile-hero {
height: 100vh;
height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
}
Because CSS ignores declarations it doesn’t understand, older browsers keep using the first 100vh line, while modern browsers override it with the more accurate 100dvh. (svh and lvh are also available for “smallest possible” and “largest possible” viewport height, if you need one extreme or the other explicitly.)
Mistake 2: Unclamped vw font sizes become unreadable
Because vw scales linearly with window width with no upper or lower bound, a heading sized purely in vw can shrink to an illegibly small size on a narrow phone or balloon absurdly large on an ultra-wide monitor:
.page-title {
font-size: 10vw;
}
On a 320px-wide phone that’s 32px (borderline acceptable), but on a 2560px ultra-wide display it becomes 256px — a heading that dwarfs the rest of the page. The fix is to wrap the viewport-based value in clamp() so it has a guaranteed floor and ceiling:
.page-title {
font-size: clamp(1.75rem, 10vw, 4rem);
}
Now the title never drops below 1.75rem or exceeds 4rem, while still scaling fluidly between those bounds as the window resizes.
Best Practices
- Prefer
clamp(min, preferred, max)over a bare viewport unit for anything text-related — it gives you fluid scaling with guardrails in one declaration. - For full-screen mobile layouts, pair
height: 100vhwith aheight: 100dvhoverride rather than relying onvhalone. - Reach for
vminwhen an element (a modal, a circular badge, a square thumbnail) must always fit on screen regardless of device orientation. - Avoid setting both
widthandheightpurely invw/vhfor interactive elements like buttons — text inside can become distorted or clipped at extreme aspect ratios; combine withmin()/max()or fixed units instead. - Remember that
100vwcan exceed the visible width if a vertical scrollbar is present, since scrollbar width is included in some browsers’ viewport calculation — this can cause unwanted horizontal overflow. Test on real devices, not just by resizing a desktop browser. - Use viewport units for large-scale, whole-page layout decisions; use rems and ems for component-level and text sizing that should respect user font-size preferences and zoom settings.
Practice Exercises
- Exercise 1: Build a full-screen landing section using
100dvh(with a100vhfallback) that centers a heading and a subheading vertically and horizontally. Verify it doesn’t get clipped when a mobile browser’s address bar is visible. - Exercise 2: Create a heading that uses
clamp()combined withvwso it reads at least 1.25rem on the smallest phones and never exceeds 3rem on large monitors. - Exercise 3: Style a circular profile avatar using
vminso it always fits within the screen regardless of whether the device is in portrait or landscape orientation, and stays perfectly circular at every size.
Summary
vwandvhare percentages of the viewport’s width and height;vmin/vmaxtrack the smaller/larger of the two dimensions, which makes them orientation-safe.- Viewport units always resolve against the root viewport, not a parent element, no matter how deeply nested the element is.
- Combine viewport units with
clamp(),min(), ormax()to get fluid scaling with a safe floor and ceiling — this is the standard pattern for responsive typography. 100vhis unreliable on mobile because it ignores the browser’s shrinking/expanding UI chrome; usedvh(with avhfallback) for accurate full-screen mobile sections.- Viewport units are recalculated live on every resize, rotation, or mobile chrome toggle, triggering a fresh layout pass.
