You build a full-screen hero, check it on a laptop, and it looks right. Then you open it on a phone and the address bar cuts the button in half.
Browsers do this on purpose. vh ignores the bars that slide in and out while you scroll, so it measures a screen you only get after scrolling. The svh, lvh, and dvh units let you choose which height you mean. This guide shows what each one measures, when to use it, and what none of them fix.
Why 100vh overflows on phones
A phone browser shows its address bar when a page loads and slides it away as you scroll down. The visible area gets taller, but vh stays the same. It is computed as if the bar were already gone.
Take a phone with 760px of height when the bar is hidden and 680px when it is showing. I will use those numbers throughout this guide. On load:
.hero {
min-height: 100vh; /* 760px, while only 680px are visible */
}
The hero is 80px taller than the screen, and a button pinned to its bottom edge stays behind the bar until the visitor scrolls.
Simulated phone: a full-screen hero on load
min-height: 100vhWhy browsers do it this way
It looks like a bug, but it was a decision. Chrome on Android used to resize vh as the address bar came and went, and the whole page reflowed each time the reader changed scroll direction. Chrome 56 froze vh at the largest possible height to stop that reflow, which also matched what Safari on iOS already did.
So there are two things you might want from a viewport unit: a size that matches the screen right now, and a size that never moves while someone is scrolling. No single unit can do both, which is why CSS now has several.
The small, large, and dynamic viewports
The CSS Values specification defines three sizes for the same screen, and each one gets its own units:
- Small,
svh: the height with the browser bars showing. In our example, 680px. It stays the same while scrolling. - Large,
lvh: the height with the bars hidden, so 760px. It also stays the same while scrolling, and todayvhgives the same value. - Dynamic,
dvh: whichever is true right now. It starts at 680px and moves toward 760px as the bars slide away.
Each size also has width and logical versions, such as svw, dvi, and lvb. You will mostly use the height units, because mobile bars take their space from the height.
Desktop browsers have no retracting bars, so all of these units return the same number there. That is why the bug survives a review on a laptop. You can check on your own device, since these columns use the real units:
Live example: measured by your browser
30vh30lvh30svh30dvh
On a laptop the columns match. On a phone, svh is the shortest, and dvh changes as you scroll.
Try each unit
Here is the same 760px phone as a small model. Pick a unit, then hide the bar the way scrolling would.
Live example: pick a unit
The hero is 760px tall and 680px are visible. The last 80px sit behind the bar. The hero is 680px tall and 680px are visible. It fits. The hero is 760px tall and 760px are visible. It fits. The hero is 680px tall and 760px are visible. The next section shows below it.
Three things to look for:
100vhand100lvhbehave the same. With the bar showing, the button is cut off.100svhkeeps the button visible in both states. When the bar hides, the next section shows below the hero.100dvhalways fills the screen, but the hero grows when the bar hides and pushes the next section down. On a real page, that happens while the reader is scrolling.
Fix a hero with svh
For anything at the top of a scrolling page, svh is the safe choice:
.hero {
min-height: 100svh;
}
The first screen now fits while the bars are showing, and the height never changes, so nothing moves during scrolling. Once the bars retract, a little of the next section appears under the hero, which is a useful hint that the page continues.
Simulated phone: before and after
min-height: 100vhmin-height: 100svhSupport is no longer a concern for most sites. These units shipped in Chrome 108, Firefox 101, and Safari 15.4, and they have been Baseline Widely available since June 2025. If you still support older browsers, declare vh first. A browser that does not know svh ignores the second line:
.hero {
min-height: 100vh;
min-height: 100svh;
}
Use min-height, not height
A fixed height creates a different bug. On a small phone, or on any phone held sideways, the content can be taller than the viewport. With height, it spills out of the section and over whatever comes next. min-height gives you the same first screen and lets the section grow when it has to.
Simulated page: the dashed line is the end of the screen
height: 100svhmin-height: 100svhWhen dvh is the right choice
Use dvh for layers that should match the visible screen at every moment and are not part of a scrolling page. A full-screen app layout is one case. A mobile menu is another: it can open before the visitor has scrolled or after, so the bars may be showing or hidden when it appears.
.app-shell {
display: grid;
grid-template-rows: auto 1fr auto;
block-size: 100dvh;
}
.mobile-menu {
max-block-size: 100dvh;
overflow-y: auto;
}
Simulated phone: the same menu in both states
Avoid it for sections inside a scrolling page. As the bars slide away, every dvh section grows and everything below it moves, right under the reader's thumb. MDN warns that this can degrade the interface and hurt performance, and browsers throttle these updates, so the resize can look jumpy.
When lvh is the right choice
lvh suits decoration that must always cover the whole screen, where being too tall does no harm. A fixed background is the typical case. With svh, a strip appears when the bars retract. With dvh, the image rescales during scrolling.
.page-background {
position: fixed;
inset-block-start: 0;
inset-inline: 0;
block-size: 100lvh;
background: url("/texture.avif") center / cover;
}
Simulated phone: a fixed background after scrolling
block-size: 100svhblock-size: 100lvhSince vh gives the same value today, you could leave it alone. I prefer lvh in new code because it says what you mean, while the size behind vh is left to the browser.
The on-screen keyboard is a separate problem
None of these units react to the on-screen keyboard by default. It does not count as a browser bar, so a 100dvh layout keeps its height and the keyboard covers the bottom of it. A chat input pinned to the bottom of the screen is the usual victim.
Chrome and Firefox on Android let you opt in to a layout that shrinks when the keyboard opens:
<meta
name="viewport"
content="width=device-width, initial-scale=1, interactive-widget=resizes-content"
/>
Safari does not support interactive-widget. If an input has to stay above the keyboard there too, you still need the Visual Viewport API.
Notches and home indicators
Viewport units also ignore the safe area. This matters when a page runs edge to edge with viewport-fit=cover, because a notch or a home indicator can sit on top of your content. Add the inset to anything pinned to the bottom:
.hero {
min-height: 100svh;
padding-block-end: max(1.5rem, env(safe-area-inset-bottom));
}
When the inset is 0, max() keeps your normal padding.
Delete the old workarounds
If your site is a few years old, it may still carry a fix from before these units existed. The most common one measures the window with JavaScript:
function setViewportHeight() {
const unit = window.innerHeight * 0.01;
document.documentElement.style.setProperty("--vh", `${unit}px`);
}
window.addEventListener("resize", setViewportHeight);
setViewportHeight();
.hero {
min-height: calc(var(--vh, 1vh) * 100);
}
It needs a script just to size a section, listens to every resize, and still changes the hero's height when the bars move, because innerHeight follows them. One declaration replaces all of it:
.hero {
min-height: 100svh;
}
The same goes for min-height: -webkit-fill-available, the CSS version of this fix.
Which unit to use
- A section in a scrolling page:
min-height: 100svh. - An app layout, a modal, or a mobile menu:
100dvh. - A fixed background that must always cover the screen:
100lvh. - Anything with text inside:
min-height, notheight. - Controls pinned to the bottom of an edge-to-edge page: add
env(safe-area-inset-bottom).
CSS Radar reports each height: 100vh and min-height: 100vh rule that has no svh or dvh version for the same selector, so you can find them without reading through every stylesheet.
