If you have ever built a full-screen hero section, opened it on your phone, and watched the bottom get swallowed by the browser’s address bar, you are not doing anything wrong. 100vh is broken on mobile by design, and it has been frustrating developers for years.
The good news is that CSS now ships a proper fix in the form of three new sets of viewport units. This guide explains exactly what they are, why they exist, and how to use them with real, copy-paste examples.
You can see all of these working live in the demo below.
Why 100vh Has Always Been Broken on Mobile
On desktop, 100vh works exactly as you would expect: it gives you 100% of the visible viewport height. On mobile, it lies to you.
Mobile browsers like Safari and Chrome have dynamic UI elements, primarily an address bar at the top and sometimes a toolbar at the bottom. These bars slide away as the user scrolls down to give more reading space, then reappear when the user scrolls back up.
The problem is that 100vh is always calculated as if those bars are already hidden, even when they are sitting right there on screen taking up space.
The result:
Your height: 100vh element is taller than the actual visible area when the page first loads. Your call-to-action button sits just below the fold, invisible behind the address bar. Your "full-screen" hero is not actually full screen.
/* This looks fine on desktop, but clips content on mobile */
.hero {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
The old workaround was a JavaScript hack that read window.innerHeight, set a CSS custom property, and re-ran on every resize event. It worked, but it caused layout shifts, depended on JS being loaded before first paint, and felt like exactly what it was: a band-aid over a browser bug.
The CSS Working Group solved this properly by defining three distinct viewport states and giving each one its own set of units.
The Three New Viewport Unit Families
Think of the mobile viewport as having two extreme states: one where the browser chrome is fully visible and one where it has fully retracted. The new units let you target either extreme or follow along dynamically in between.
Large Viewport Units (lv)
lvh, lvw, lvmin, lvmax are calculated assuming all browser UI is retracted and out of the way. This gives you the largest possible viewport size. It is equivalent to what 100vh was always pretending to be.
.fullscreen-modal {
position: fixed;
inset: 0;
width: 100lvw;
height: 100lvh;
background: rgba(0, 0, 0, 0.85);
display: grid;
place-items: center;
}
This is the right unit for modals and overlays. When a modal opens, the user is focused on it and not actively scrolling, so the browser chrome is usually retracted anyway. Using lvh means the overlay truly covers the entire screen without leaving a gap at the bottom.
Small Viewport Units (sv): When Chrome Is Fully Visible
svh, svw, svmin, svmax are calculated assuming all browser UI is fully expanded. This is the minimum guaranteed space you will ever have, the portion of the screen that is always visible no matter what the browser is doing.
.sticky-cta-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100svw;
padding: 1rem;
background: #1a1a2e;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
This is the right unit for anything that absolutely must be visible at all times. Sticky headers, cookie banners, bottom navigation bars, and floating action buttons all benefit from sv units because you know with certainty they will never be clipped by browser UI.
Dynamic Viewport Units (dv): The Real-Time Middle Ground
dvh, dvw, dvmin, dvmax update in real time as the browser Chrome appears and disappears. When the address bar is visible, dvh equals svh. When the user scrolls, the address bar retracts, dvh expands to equal lvh. It is always an exact match for the actual visible area.
.hero {
height: 100dvh;
display: grid;
place-items: center;
background: linear-gradient(135deg, #1a1a2e, #16213e);
}
This is what most developers reach for first as a 100vh replacement, and it works beautifully for hero sections and full-screen app layouts. One thing to be aware of: dvh does not animate at 60fps. Browsers intentionally debounce the updates so the layout does not jitter as the chrome slides. The transition is smooth enough that users never notice, but it means you should not tie scroll animations directly to dvh changes.
Real-World Examples
The Perfect Mobile Hero Section
The most common use case. The goal is a section that fills exactly the visible screen when the user arrives, with no content hidden below the fold.
.hero {
height: 100svh;
/* Sized to the guaranteed visible area on first load */
display: grid;
place-items: center;
padding: 2rem;
background-image: url('/hero-bg.jpg');
background-size: cover;
background-position: center;
text-align: center;
}
.hero__title {
font-size: clamp(2rem, 5vw, 4rem);
color: white;
line-height: 1.2;
}
.hero__cta {
margin-top: 2rem;
padding: 1rem 2.5rem;
background: #e94560;
color: white;
border: none;
border-radius: 50px;
font-size: 1.1rem;
cursor: pointer;
}
Why svh instead of dvh here? Because when a user first lands on your page, the browser chrome is visible. svh sizes the hero to that exact visible area, so your headline and CTA are always fully in frame on first impression. dvh would be slightly too tall at that moment, potentially pushing the CTA just out of view.
A Native-Feeling Mobile App Shell
Progressive web apps and mobile-first interfaces need a layout that feels native. The three-row grid (header, scrollable content, bottom nav) is the standard pattern, and dvh is the perfect unit for it.
.app-shell {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100dvh;
/* Always matches the actual visible screen */
overflow: hidden;
/* Prevent the shell itself from scrolling */
}
.app-header {
height: 56px; background: #1a1a2e;
color: white;
display: flex;
align-items: center;
padding: 0 1rem;
gap: 1rem;
}
.app-content {
overflow-y: auto;
/* Only this region scrolls */
padding: 1rem;
-webkit-overflow-scrolling: touch;
}
.app-nav {
height: 64px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: space-around;
align-items: center;
background: white;
}
As the user scrolls through .app-content and the browser chrome slides away, the shell expands smoothly with dvh to fill the newly available space. The header and bottom nav stay anchored and fully visible at all times.
A Side Drawer Navigation
Drawers need to be exactly as tall as the visible viewport, no more, no less. svh is the right call here because the drawer usually opens while the user is still at the top of the page with the chrome visible.
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 200;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.drawer {
position: fixed;
top: 0;
left: 0;
width: min(320px, 85svw);
height: 100svh;
/* Never taller than what the user can see */
background: white;
z-index: 201;
overflow-y: auto;
padding: 2rem 1.5rem;
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.drawer.is-open {
transform: translateX(0);
}
.drawer.is-open+.drawer-overlay {
opacity: 1;
pointer-events: all;
}
Fallback for Older Browsers
Support for these units is excellent across modern browsers (Chrome 108+, Safari 15.4+, Firefox 101+), but if you need to cover older versions, the CSS cascade handles it cleanly with no JavaScript required:
.hero {
height: 100vh;
/* Picked up by browsers that don't know dvh */
height: 100dvh;
/* Overrides above in modern browsers */
}
.sticky-bar {
width: 100vw;
width: 100svw;
}
Since browsers silently ignore CSS declarations they do not understand, this two-line pattern gives you the new behavior everywhere it is supported and a reasonable fallback everywhere else.
Choosing the Right Unit at a Glance
The decision comes down to one question: what should your element look like when the browser chrome is fully visible?
If you want the element to fill the screen regardless of browser chrome, use lv units. The element will be as large as possible and may extend behind browser UI when it is visible. Ideal for modals and overlays that take over the full screen.
If you want the element to always fit within the guaranteed visible area, use sv units. The element will never be clipped by browser UI, even when the address bar is fully expanded. Ideal for sticky bars, banners, and navigation.
If you want the element to always match exactly what the user sees right now, use dv units. The element adjusts as the chrome animates. Ideal for hero sections, app shells, and scrollable full-screen layouts.
When you are not sure which to use, svh is the safest default. It guarantees your content is always fully visible, which is almost always what you actually want.
The Bottom Line
The 100vh mobile bug has been an annoyance for the better part of a decade. These new units are not a workaround or a polyfill. They are a proper, native CSS solution that describes browser viewport behavior precisely. Swap your hero sections to svh or dvh, your sticky elements to svh, and your modals to lvh. It is a small change that makes a real difference for every mobile visitor on your site.
Did you learn something good today?
Then show some love.
© Muhammad Usman
WordPress Developer | Website Strategist | SEO Specialist
Don’t forget to subscribe to Developer’s Journey to show your support.

Top comments (0)