Why Viewport Units Behave Strangely on Mobile
Viewport units have always been controversial, and some of that is because of how mobile browsers have made things more complicated by having their own opinions about how to implement them. When building responsive mobile layouts, developers often encounter unexpected behavior where 100vh doesn't actually mean 100% of the visible viewport height.
The issue stems from how browsers interpret the "viewport" definition. On desktop browsers, the viewport is relatively stable, but on mobile devices, the visible area changes as users interact with the page. When you set an element to height: 100vh, you're not necessarily getting an element that fills 100% of what the user can currently see.
The CSS specification is remarkably vague about how viewport units should be calculated on mobile devices. Mobile browsers like Safari and Chrome have address bars and navigation controls that appear and disappear as the user scrolls, which creates complications for fixed-height elements using viewport units.
For developers building responsive web applications, understanding these nuances is essential for creating consistent user experiences across all devices. This is particularly important when implementing mobile-first design principles that prioritize smaller screens while maintaining functionality on desktop.
The Problem with Traditional Viewport Units
When you're working with viewport height (vh) on a mobile browser, you're dealing with an environment where the visible viewport changes dynamically. Different mobile browsers handle viewport units in distinct ways:
- Safari for iOS defines a fixed value for
vhbased on the maximum height of the screen, preventing content jumping but potentially cropping elements when the address bar is visible - Chrome's mobile browser adopted a similar approach, though implementations aren't identical across all platforms
- Firefox for Android has been working on addressing this behavior, with implementation details continuing to evolve
The result is that 100vh may refer to the maximum possible viewport rather than the current visible area, causing elements to be cropped or extend beyond the visible screen.
According to CSS-Tricks' analysis of mobile browser viewport behavior, this inconsistency has been a persistent challenge for developers building responsive mobile layouts.
The Classic Solution: CSS Custom Properties with JavaScript
The solution that became widely known as "the trick to viewport units on mobile" involves using CSS custom properties (CSS variables) in combination with JavaScript to dynamically calculate and apply the correct viewport height.
The technique works by capturing the current viewport height using JavaScript's window.innerHeight property, which accurately reflects the currently visible area of the browser, including accounting for any visible browser chrome.
The CSS
.element {
height: 100vh; /* Fallback for browsers that don't support custom properties */
height: calc(var(--vh, 1vh) * 100);
}
The JavaScript
// First we get the viewport height and multiply it by 1% to get a value for a vh unit
let vh = window.innerHeight * 0.01;
// Then we set the value in the --vh custom property to the root of the document
document.documentElement.style.setProperty('--vh', `${vh}px`);
The --vh custom property now represents 1% of the current visible viewport height, and multiplying it by 100 gives you exactly 100% of the visible area.
This approach is particularly valuable for cross-platform mobile applications that need consistent behavior across iOS and Android browsers. This technique, as documented by CSS-Tricks, provides precise control over viewport calculations regardless of browser implementation.
1.my-element {2 height: 100vh; /* Fallback */3 height: calc(var(--vh, 1vh) * 100);4}5 6/* Alternative: Using directly */7.fullscreen-section {8 height: calc(var(--vh, 1vh) * 100);9 min-height: calc(var(--vh, 1vh) * 100);10}1// Calculate and set viewport height custom property2function setViewportHeight() {3 let vh = window.innerHeight * 0.01;4 document.documentElement.style.setProperty('--vh', `${vh}px`);5}6 7// Set on initial load8setViewportHeight();9 10// Update on window resize11window.addEventListener('resize', setViewportHeight);Handling Window Resize and Orientation Changes
Mobile devices can be rotated, changing the viewport dimensions significantly. To handle these scenarios, the solution should include a resize event listener:
window.addEventListener('resize', () => {
let vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
});
Important consideration: Continuously updating the --vh value during scroll-triggered address bar changes can cause visible layout shifts and performance issues. For most use cases, it's better to calculate the viewport height once on page load and only update it when the window is actually resized or rotated.
As noted by CSS-Tricks, you should carefully consider performance implications and implement appropriate throttling or debouncing when handling resize events frequently.
| Unit | Definition | Use Case | Potential Issue |
|---|---|---|---|
| vh | Traditional viewport height | Desktop layouts | Inconsistent on mobile |
| dvh | Dynamic viewport height | Always fill visible area | Layout shifts on scroll |
| svh | Small viewport height | Elements must be fully visible | May appear too small |
| lvh | Large viewport height | Maximum screen coverage | May extend under chrome |
| CSS + JS | Custom property with JavaScript | Cross-browser compatibility | Requires JavaScript |
Key recommendations for reliable cross-device viewport unit implementation
Test on Real Devices
Browser DevTools responsive mode doesn't perfectly replicate mobile browser behavior. Always test on actual iOS and Android devices.
Provide Fallbacks
When using modern viewport units, provide fallbacks for browsers that don't support them.
Consider Accessibility
Layout shifts can affect users with motion sensitivity. Consider using prefers-reduced-motion.
Use Units Appropriately
Viewport units aren't always right for all layouts. Consider flexbox/grid for more dynamic sizing.
1/* Progressive enhancement with fallbacks */2.hero-section {3 /* Fallback for older browsers */4 height: 100vh;5 6 /* Modern browsers: small viewport height */7 height: 100svh;8 9 /* Even more modern: dynamic viewport height */10 height: 100dvh;11}