CSS Viewport Units Explained: vw, vh, dvh, svh, and lvh for Responsive Design
Viewport units size things relative to the browser window rather than a parent element or the root font-size, which makes them useful for full-bleed heroes, fluid type, and anything meant to track the screen directly. They also came with one especially well-known mobile bug — and CSS eventually shipped new units specifically to fix it.
The original four
| Unit | Meaning |
|---|---|
vw | 1% of the viewport's width |
vh | 1% of the viewport's height |
vmin | 1% of whichever is smaller, viewport width or height |
vmax | 1% of whichever is larger, viewport width or height |
vmin is the useful one for "scale with the screen regardless of orientation" — a square
element sized in vmin stays proportionally sized whether the device is held in portrait or
landscape, since it always tracks the smaller dimension.
The 100vh mobile bug
A section set to height: 100vh is meant to fill exactly one screen. On mobile Safari (and
historically some other mobile browsers), it didn't — because the browser's address bar and toolbar can
collapse or expand as the user scrolls, changing how much space is actually available, and 100vh
was defined against the largest possible viewport (toolbars collapsed). The practical effect: a
full-height hero would be taller than the visible screen the moment the address bar was showing, cutting
off content at the bottom or causing an unwanted scrollbar on a page that was supposed to be exactly one
screen tall.
The fix: dvh, svh, and lvh
Modern CSS defines three more precise viewport-height units that resolve the ambiguity directly:
| Unit | Meaning |
|---|---|
svh | Small viewport height — the shortest the viewport gets (toolbars fully expanded) |
lvh | Large viewport height — the tallest the viewport gets (toolbars collapsed); matches old vh behavior |
dvh | Dynamic viewport height — tracks the actual current state, live, as toolbars show or hide |
The matching width variants (svw, lvw, dvw) exist too, though the
height case is the one that actually caused visible bugs, since horizontal toolbars are rare.
.hero {
min-height: 100svh; /* never taller than the shortest real viewport */
}
Using svh for a full-height hero guarantees the section never exceeds what's visible even in
the most toolbar-heavy state, which is the safer default for "this must fit on one screen, no scrolling."
dvh is the right choice when you want the section to actively resize as the toolbar animates —
more visually responsive, but it does mean the layout shifts slightly as the user scrolls, which is worth
testing on a real device before committing to it for anything with precise internal spacing.
Browser support
dvh/svh/lvh have been supported in all major browser engines since
2023 and are safe to use as a progressive enhancement — pair a fallback plain vh declaration
first, then the dynamic unit after it, so older browsers get the old (imperfect but functional) behavior
and newer ones get the corrected sizing:
.hero {
min-height: 100vh; /* fallback */
min-height: 100svh; /* modern browsers override with this */
}
A full-screen modal that actually fits
A mobile-style full-screen modal or sheet is one of the clearest real cases where the old vh
bug caused visible problems — content or a close button pinned to the bottom of a 100vh panel
could end up hidden behind the browser's toolbar. Swapping in the dynamic unit fixes it directly:
.mobile-sheet {
position: fixed;
inset: 0;
height: 100vh; /* fallback for older browsers */
height: 100dvh; /* tracks the real, current viewport as toolbars show/hide */
overflow-y: auto;
}
Because dvh updates live as the toolbar animates, the sheet's bottom edge — and anything
pinned to it — stays exactly at the visible edge of the screen rather than the pre-bug assumption of a
toolbar-collapsed viewport.
Why plain vw alone is risky for font-size
It's tempting to write font-size: 4vw directly instead of a full clamp()
expression, but a bare viewport-unit value has no floor or ceiling — it keeps shrinking on a narrow phone
until it's illegibly small, and keeps growing on an ultra-wide monitor until it's disproportionately large.
Viewport units are the right building block for fluid sizing, but they're almost always meant to be paired
with a minimum and maximum via clamp() (see the
clamp() guide) rather than used bare.
Testing on a real device, not just devtools
Browser devtools' mobile emulation mode is a good first check but doesn't reproduce the actual toolbar
show/hide behavior that caused the original 100vh bug — that specific interaction only really shows up
scrolling a real mobile browser by hand. For anything relying on dvh/svh/lvh
specifically to solve a toolbar-related layout problem, a real-device check (or at minimum a remote-debugging
session against one) is worth the extra few minutes before shipping.
Translating between px and viewport units
Because a vw or vh value depends entirely on the current screen size, "what does 5vw actually render as" only has an answer once you pick a viewport width. Checking that translation against real device widths — common breakpoints like 375px (a typical phone), 768px (tablet), and 1440px (desktop) — is exactly what the viewport units calculator does, with device presets so you're not guessing at a screen size from memory.
Viewport units and orientation changes
Rotating a phone from portrait to landscape swaps which dimension is larger, which is exactly why a
fixed vh value can look right in one orientation and wrong in the other — a hero sized to
60vh in portrait becomes a much shorter section once width and height effectively swap in
landscape, because the underlying viewport height dropped significantly. vmin sidesteps this
specific problem for anything that should hold a consistent visual weight regardless of orientation, since
it always tracks whichever dimension is currently smaller.
Viewport units inside nested, scrollable containers
It's a common surprise that vh/vw always reference the top-level browser
viewport, never a scrollable parent container the element happens to sit inside. An element nested inside
a fixed-height, independently-scrolling sidebar sized with height: 50vh is sized against the
whole page's viewport height, not the sidebar's own height — for sizing relative to a specific container
rather than the page, container query units (cqh, cqw) or a percentage against
an explicitly-sized parent are the correct tool instead.
Frequently Asked Questions
Should I replace every vh with dvh?
Not necessarily — for anything not meant to be exactly full-screen (a section that's simply "large," not
strictly viewport-height-locked), plain vh is fine. Reach for the dynamic units specifically
where mobile toolbar resizing has caused a visible layout problem.
Do desktop browsers benefit from dvh/svh/lvh?
Marginally — desktop toolbars don't typically resize the same way, so vh, svh,
and lvh usually compute identically there. The real benefit is mobile.
What's the difference between vmin and the smaller of vw/vh manually?
None functionally — vmin is exactly that, computed for you, and it updates automatically on
orientation change without any JavaScript.
Does 100dvh ever cause layout jumpiness?
It can, slightly, since the value is genuinely recalculating as the toolbar animates — for elements with
sensitive internal spacing, testing the transition on a real device is worth it before deciding between the
live-updating dvh and the more stable (but sometimes over-tall) lvh/svh.
Do viewport units work inside a CSS calc() expression?
Yes — calc(100dvh - 4rem) to account for a fixed header, for example, works exactly like any
other length combination inside calc().