Mobile Viewport Units and the iOS 100vh Bug
The notorious iOS Safari bug with 100vh has kept developers busy for years. The modern CSS viewport units dvh, svh and lvh solve the problem permanently, with no JavaScript, no resize events and no fragile workarounds. This article explains how Dynamic, Small and Large Viewport Units work and when you actually need which one.
Table of Contents
- 1. The 100vh Problem on Mobile Browsers
- 2. svh: Small Viewport Height
- 3. lvh: Large Viewport Height
- 4. dvh: Dynamic Viewport Height
- 5. Viewport Widths: svw, lvw, dvw
- 6. Practical Examples: Hero Sections and Modals
- 7. Fallback Strategies and Progressive Enhancement
- 8. Viewport Units Compared Side by Side
- 9. Performance and Layout Stability
- 10. Summary
- 11. FAQ
1. The 100vh Problem on Mobile Browsers
Anyone who has ever built a full-screen hero section or a mobile overlay with height: 100vh knows the problem: on iOS Safari and older Android browsers, the height gets calculated incorrectly. The browser measures the viewport without its own UI chrome, the address bar, tab bar and bottom navigation, which means 100vh ends up larger than the area that is actually visible. The bottom edge of the element disappears behind the browser UI. This is not a bug in the classic sense but a deliberate design choice: Apple wanted a smooth UI animation while scrolling, with the browser chrome sliding in and out. The resulting gap between the CSS viewport height and the visible area has haunted developers since iOS 8.
The classic workaround was JavaScript: a resize event listener sets a CSS custom property based on window.innerHeight, which is then used instead of 100vh. The pattern is well known but fragile. It requires JavaScript at first paint, triggers a layout reflow every time the event fires and stops working if JavaScript is blocked or delayed. The modern CSS dvh, svh and lvh units offer a purely CSS-based solution that removes all of these downsides. They were standardized in the CSS specification in 2022 and are now broadly supported starting with Chromium 108, Safari 15.4 and Firefox 101.
2. svh: Small Viewport Height
svh (Small Viewport Height) matches the height of the viewport when the browser UI is fully expanded, in other words when every bar is visible and the available area is at its smallest. On an iPhone with Safari in its default state, 100svh shows exactly the height that is actually available to the user while the address bar is fully visible. svh is therefore the most conservative option: an element with height: 100svh always fits inside the viewport, no matter what state the browser UI is in.
The unit is a great fit for elements that must be guaranteed to be fully visible, regardless of whether the browser chrome is currently expanded or collapsed. Typical use cases are dialogs, modals and overlays where it is critical that no content disappears behind the browser UI. CSS svh is the safe choice whenever complete visibility matters more than making maximum use of the available space. One downside: as the browser chrome slides in and out, a visible jump can occur, because 100svh is then smaller than the area that is actually available.
/* Small Viewport Height: conservative, always fully visible */
.modal-overlay {
/* Height based on the smallest possible viewport (UI fully visible) */
height: 100svh;
overflow-y: auto;
}
.sticky-footer-nav {
/* Position relative to small viewport bottom */
position: fixed;
bottom: 0;
left: 0;
right: 0;
/* Ensure content above nav is visible within small viewport */
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* Minimum height that works in all browser UI states */
.hero-safe {
min-height: 100svh;
display: flex;
align-items: center;
}
/* Container that must not be clipped by browser chrome */
.fullscreen-dialog {
width: 100svw;
height: 100svh;
position: fixed;
inset: 0;
}
One important difference from 100vh: svh is a static value that does not change while scrolling. The browser calculates it once, based on the smallest possible viewport state, and keeps that value. This prevents layout reflows during scrolling, which makes CSS svh more attractive from a performance standpoint than dvh whenever dynamic adjustment is not actually needed.
3. lvh: Large Viewport Height
lvh (Large Viewport Height) is the opposite of svh: it matches the height of the viewport when the browser UI is fully collapsed, in other words when the maximum amount of space is available for content. This is the state most users see while scrolling through a page: the address bar has disappeared, the bottom tab bar has retracted, and the content fills the entire screen. 100lvh therefore corresponds to what 100vh means on desktop browsers without dynamic chrome.
The use case for CSS lvh is narrower than for svh or dvh. It suits decorative elements such as background images or videos, where a little clipping at the edge is not a problem and where you want to make use of the maximum image area. The catch: on the initial page view, while the browser chrome is still visible, an element with height: 100lvh can extend beyond the visible area. That leads to unwanted scroll behavior or clipped content. lvh is therefore rarely the first choice for interactive or content-bearing elements.
4. dvh: Dynamic Viewport Height
dvh (Dynamic Viewport Height) is the smartest of the three units. It adapts dynamically to the current state of the browser UI: when the address bar is visible, 100dvh equals 100svh; when it is collapsed, it equals 100lvh. That sounds like the ideal solution, and for many use cases it is. The unit behaves the way developers intuitively expect 100vh to behave on mobile devices.
The price for that flexibility is a possible layout reflow during scrolling. When a user scrolls and the browser chrome expands or collapses, the computed value of dvh changes. Elements that use this value for their height get recalculated and possibly re-rendered. For simple containers that is a non-issue. For complex layouts with many dependent sizes it can cause visible flickering or jumping. For hero sections and full-screen elements, where adapting to the current viewport state matters more than layout stability, CSS dvh remains the recommended choice.
/* Dynamic Viewport Height: adapts to current browser UI state */
.hero-section {
/* Fills exactly the visible area, regardless of browser chrome state */
min-height: 100dvh;
display: grid;
place-items: center;
}
/* Fullscreen mobile menu */
.mobile-nav-overlay {
position: fixed;
inset: 0;
/* Dynamic: correct height when menu opens mid-scroll */
height: 100dvh;
overflow-y: auto;
overscroll-behavior: contain;
}
/* Large Viewport for decorative backgrounds */
.bg-video-container {
/* Use lvh for decorative elements where overflow is acceptable */
min-height: 100lvh;
position: relative;
overflow: hidden;
}
/* Progressive enhancement: cascade from old to new */
.fullscreen-element {
height: 100vh; /* Fallback for old browsers */
height: 100dvh; /* Dynamic viewport: modern browsers */
}
/* Fluid typography scaled to viewport height */
.hero-headline {
font-size: clamp(2rem, 8dvh, 5rem);
}
5. Viewport Widths: svw, lvw, dvw
The same three-way split exists for width too: svw, lvw and dvw mirror the same concepts on the horizontal axis. In practice, the difference between the three width variants is less noticeable on most devices, since browsers rarely expand and collapse their horizontal chrome dynamically. The main exceptions are browsers with side panels or certain PWA modes. Still, the full set exists: svw, lvw, dvw as well as the inline and block variants svi, lvi, dvi, svb, lvb, dvb.
Especially relevant for modern layouts is combining dvh and dvw for truly full-screen elements. With width: 100dvw; height: 100dvh; and position: fixed; inset: 0; you get an element that exactly covers the visible area, at any point in time and in any browser UI state. The new units also complement the existing vmin and vmax variants, which now have counterparts too: svmin, lvmin, dvmin as well as svmax, lvmax and dvmax.
6. Practical Examples: Hero Sections and Modals
The most common use cases for CSS dvh, svh and lvh are full-screen hero sections, mobile navigation overlays and modals. For hero sections, min-height: 100dvh is the recommended choice: the element fills the visible area in every state but still grows if the content needs more space. For modals that absolutely must be fully visible, height: 100svh is the safest choice, since it stays correct even while the browser chrome is visible.
Another important use case is calculating sticky elements and scroll-snap containers. With CSS dvh you can build scroll-snap sections that always occupy exactly the visible area, regardless of whether the address bar happens to be visible or not. That produces a noticeably more consistent scroll experience on mobile than the JavaScript-based approach.
/* Scroll-snap fullscreen sections using dvh */
.snap-container {
height: 100dvh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
overscroll-behavior-y: contain;
}
.snap-section {
height: 100dvh; /* Each section fills exactly the visible viewport */
scroll-snap-align: start;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Modal: always fully visible, even when browser chrome is showing */
.modal-wrapper {
position: fixed;
inset: 0;
height: 100svh; /* svh: conservative, never clipped by browser UI */
display: grid;
place-items: center;
background-color: rgb(0 0 0 / 0.6);
}
.modal-content {
max-height: calc(100svh - 4rem); /* Leave breathing room inside safe viewport */
overflow-y: auto;
border-radius: 1rem;
}
/* Sticky sidebar that respects dynamic viewport */
.sidebar {
position: sticky;
top: 0;
height: 100dvh;
overflow-y: auto;
}
7. Fallback Strategies and Progressive Enhancement
Even though CSS dvh, svh and lvh are now supported by all modern browsers, a fallback strategy for older browsers is still worthwhile. The simplest pattern is a cascade: declare the old unit first, then the new one right after. Browsers that don't recognize the new unit simply ignore it and use the fallback. Browsers with support override the value. This pattern works with no feature detection and no JavaScript.
For projects that still need to support much older browsers, the well-established CSS custom property trick is available as extra insurance. The custom property --vh gets set via JavaScript to window.innerHeight * 0.01 and is then used as calc(var(--vh, 1vh) * 100). This approach produces correct values on every device but carries the downside of a JavaScript dependency. With CSS dvh as the primary declaration and 100vh as the fallback, this requirement disappears entirely for most current project needs.
8. Viewport Units Compared Side by Side
Choosing the right CSS viewport unit depends on the concrete use case. The table below summarizes the key differences and gives recommendations for typical scenarios.
| Unit | Based On | Layout Reflow | Best Used For |
|---|---|---|---|
100vh |
UA-dependent (often without chrome) | No | Desktop, fallback for old browsers |
100svh |
Smallest viewport (UI visible) | No | Modals, dialogs, critical overlays |
100lvh |
Largest viewport (UI hidden) | No | Decorative backgrounds, images |
100dvh |
Current viewport (dynamic) | Possible while scrolling | Hero sections, scroll-snap, sidebars |
JS --vh |
window.innerHeight |
On resize event | Legacy browsers as a last resort |
For most modern projects, min-height: 100dvh is the recommended default for full-screen elements on mobile. CSS svh comes into play whenever guaranteed full visibility matters more than making exact use of the available space. lvh remains a special case for decorative elements where minor overflow is acceptable.
9. Performance and Layout Stability
From a performance standpoint, CSS dvh is the only one of the new units that can trigger recalculations while scrolling. When the browser chrome expands or collapses, every element using dvh-based values has to be recalculated. On modern devices and browsers this overhead is minimal and barely measurable in practice. That said, if you want to avoid layout instability altogether, prefer svh or lvh, depending on which viewport state should be the basis.
In the context of Core Web Vitals, Cumulative Layout Shift (CLS) is especially relevant here. Elements whose height is driven by dvh and that don't yet have a stable size during the initial load phase can contribute to CLS. The solution is to set the initial value with svh and only use dvh for state changes after the first paint, for example via a CSS class applied after the initial load. For most hero sections this extra effort is not necessary, since min-height: 100dvh without a hard fixed size is CLS-neutral.
Mironsoft
Mobile-first CSS, responsive design and performance optimization
Want mobile layouts that look right on every device?
We audit existing layouts for mobile viewport issues, replace fragile workarounds with modern CSS solutions and make sure hero sections, modals and full-screen elements render correctly on iOS Safari, Android Chrome and every modern browser.
Viewport Audit
Analysis of every vh usage and identification of iOS Safari issues in your existing code
CSS Migration
Replacing JS workarounds with dvh, svh and lvh, complete with correct fallbacks
CLS Optimization
Reducing layout shift through correct viewport units and stable initial layouts
10. Summary
The three modern CSS viewport units dvh, svh and lvh solve the long-standing 100vh problem on mobile browsers for good, with no JavaScript required. dvh adapts dynamically to the current viewport state and is the best choice for hero sections and scroll-snap layouts. svh is based on the smallest possible viewport and guarantees that elements are always fully visible, making it ideal for modals and critical overlays. lvh uses the largest viewport and suits decorative background elements where minor overflow is acceptable.
The recommended fallback strategy is simple: declare 100vh first, then 100dvh on the next line. Older browsers ignore the unknown unit; modern browsers override the value. For most modern projects, min-height: 100dvh is the only change needed to eliminate iOS Safari 100vh issues: a single line of CSS replaces a whole chunk of JavaScript.
CSS dvh, svh, lvh: The Essentials at a Glance
dvh: Dynamic Viewport
Adapts to the browser chrome state. Best choice for hero sections and scroll-snap. Possible reflow while scrolling.
svh: Small Viewport
Based on the smallest viewport (chrome visible). Guarantees full visibility. Ideal for modals and overlays.
lvh: Large Viewport
Based on the largest viewport (chrome hidden). Suits decorative backgrounds. Can extend beyond the visible area.
Fallback Pattern
height: 100vh; height: 100dvh; the cascade is enough. No JavaScript needed for modern browser support.