Skip to main content
Mobile-First Development

Engineering Mobile-First Experiences: Advanced Strategies for Dynamic Viewport Management

The viewport is the most lied-about rectangle in web development. For years, we told ourselves that <meta name="viewport" content="width=device-width, initial-scale=1"> was the magic incantation. It worked — until it didn’t. Foldables, notch screens, floating virtual keyboards, and multi-window modes have fractured the viewport into multiple overlapping rectangles. If you’re still treating the viewport as a fixed size, your mobile-first designs are already broken. This guide is for developers who know the basics and need to control the dynamic viewport in production without resorting to hacks that break every other browser. The Real Problem: Visual vs. Layout Viewport Most developers think there is one viewport. There are at least two, and they often disagree. The layout viewport is the rectangle into which your CSS lays out elements — it’s what width: 100vw references.

The viewport is the most lied-about rectangle in web development. For years, we told ourselves that <meta name="viewport" content="width=device-width, initial-scale=1"> was the magic incantation. It worked — until it didn’t. Foldables, notch screens, floating virtual keyboards, and multi-window modes have fractured the viewport into multiple overlapping rectangles. If you’re still treating the viewport as a fixed size, your mobile-first designs are already broken. This guide is for developers who know the basics and need to control the dynamic viewport in production without resorting to hacks that break every other browser.

The Real Problem: Visual vs. Layout Viewport

Most developers think there is one viewport. There are at least two, and they often disagree. The layout viewport is the rectangle into which your CSS lays out elements — it’s what width: 100vw references. The visual viewport is the portion of the page actually visible on screen, which shrinks when the browser chrome appears or when a virtual keyboard opens. On iOS Safari, the visual viewport can be significantly smaller than the layout viewport during keyboard interactions, causing fixed elements to float off-screen or overlap content.

Why does this matter? Consider a chat application with a fixed input bar at the bottom. On Android, the keyboard typically resizes the layout viewport, so your fixed bar stays above the keyboard. On iOS, the keyboard does not resize the layout viewport — it simply covers the bottom of the screen. The fixed element remains at the bottom of the layout viewport, which is now hidden behind the keyboard. Users can’t see what they’re typing. This is the classic iOS keyboard bug, and it’s still alive in 2025.

The Two-Viewport Model in Practice

To manage this, you must track both viewports. The visualViewport API (available in modern browsers) gives you real-time dimensions and offset. You can listen to its resize and scroll events to adjust your layout. For example, when the visual viewport height shrinks, you can reposition a fixed footer using transform: translateY() or by updating the bottom property with JavaScript. But this comes with performance pitfalls — each event can trigger layout thrashing if you’re not careful.

Another approach is to use the new CSS viewport units: dvh (dynamic viewport height), svh (small viewport height), and lvh (large viewport height). These units adapt to the visual viewport changes. dvh is particularly useful for full-screen elements because it updates as the browser chrome shows or hides. However, browser support is still spotty, and the units behave differently across platforms. For instance, on Chrome Android, dvh may not account for the keyboard, while on Safari iOS it does — exactly the opposite of what you might expect.

Common Mistakes with Viewport Units

A frequent error is using 100vh for a hero section and expecting it to fill the screen without scrollbars. On mobile, 100vh includes the address bar area, so the hero extends below the fold, forcing a scroll. The fix used to be -webkit-fill-available or JavaScript-based height calculation, but dvh is now the cleaner solution. However, dvh can cause layout shifts as the user scrolls and the chrome collapses. The trade-off is between a perfect initial fit and a jittery experience. We recommend using svh for critical above-the-fold content and dvh for immersive elements where a slight resize is acceptable.

Dynamic Viewport Units: dvh, svh, lvh

The CSS Working Group introduced dynamic viewport units to address the instability of vh. svh represents the smallest possible viewport height (typically when the browser chrome is fully expanded), lvh the largest (chrome hidden), and dvh the current dynamic value that changes as the chrome shows or hides. These units are especially useful for mobile-first designs where the address bar and toolbars appear and disappear.

But here’s the catch: dvh updates during scroll, which can cause repaints and layout shifts. If you use height: 100dvh on a fixed background, the background will resize as the user scrolls, potentially creating a jarring visual jump. For most content, svh is safer because it gives you the guaranteed smallest height — content will never be cut off, though you may have extra space when the chrome is hidden. Use dvh only for elements that should be truly full-screen at all times, like a video player or a modal overlay, and be prepared to handle the resize smoothly with CSS transitions.

Browser Support and Quirks

As of early 2025, dvh is supported in Safari 15.4+, Chrome 108+, and Firefox 101+. But support doesn’t mean consistent behavior. On Safari iOS, dvh updates when the keyboard appears, which is useful for chat apps. On Chrome Android, dvh does not update for the keyboard — it only responds to browser chrome. This inconsistency means you cannot rely on dvh alone for keyboard handling. You still need the visualViewport API for cross-platform keyboard detection.

Another quirk: on desktop browsers with developer tools open, dvh may behave unexpectedly because the browser chrome includes the dev tools panel. Test thoroughly on real devices, not just emulators.

Practical Decision Matrix

Use CaseRecommended UnitWhy
Hero section (no scroll)svh or 100vh with min-heightPrevents content from being cut off when chrome is visible
Full-screen modaldvhAdapts to chrome changes, keeps modal truly full-screen
Chat input fixed at bottomvisualViewport + dvh fallbackdvh alone fails on Android for keyboard
Background image or videosvhAvoids layout shifts during scroll
Sticky headertop: env(safe-area-inset-top)Notch and status bar handling, not viewport units

Handling Virtual Keyboards Without Breaking Fixed Elements

The virtual keyboard is the single biggest challenge in dynamic viewport management. On iOS, the keyboard does not resize the layout viewport — it simply overlays the page. This means position: fixed elements at the bottom remain fixed to the layout viewport bottom, which is now behind the keyboard. The user cannot see the input field or the send button.

The most reliable solution is to use the visualViewport API to detect when the keyboard is open (by comparing visualViewport.height to the window inner height) and then adjust the fixed element’s position. A common pattern is to set the element’s bottom property to visualViewport.offsetTop (the distance from the top of the layout viewport to the top of the visual viewport) plus a small margin. This effectively pins the element to the bottom of the visible area.

Code Snippet: Keyboard-Aware Fixed Footer

const footer = document.getElementById('chat-footer');
if (window.visualViewport) {
  const handleViewportChange = () => {
    const offset = window.innerHeight - visualViewport.height;
    if (offset > 0) {
      // Keyboard likely open
      footer.style.bottom = offset + 'px';
    } else {
      footer.style.bottom = '0';
    }
  };
  window.visualViewport.addEventListener('resize', handleViewportChange);
}

This works on both iOS and Android, but note that on Android, the keyboard typically resizes the layout viewport, so offset may be 0. You may need to also check visualViewport.height against a threshold. A more robust approach is to compare the ratio of visual viewport height to window inner height — if it drops below 0.8, assume the keyboard is open.

One pitfall: on iOS, when the keyboard is dismissed, the resize event fires multiple times, and the visual viewport height may briefly jump to an intermediate value. Debouncing the handler (e.g., 100ms) prevents flickering. Also, avoid using position: fixed on elements that need to be keyboard-accessible; consider using position: sticky with a bottom value, though sticky has its own issues on iOS.

Alternative: The scrollIntoView Hack

Some developers advocate for scrolling the input into view and letting the browser handle the rest. On iOS, scrollIntoView does not always work because the keyboard may cover the target element. A more reliable method is to programmatically set scrollTop to a large value after focusing the input, but this can cause a jarring jump. We prefer the visualViewport approach for its precision.

Detecting Viewport Changes: ResizeObserver vs. visualViewport

You have two main tools for detecting viewport changes: ResizeObserver and the visualViewport API. They serve different purposes. ResizeObserver watches element size changes, which includes the root element (document.documentElement). When the keyboard opens on Android, the layout viewport resizes, so ResizeObserver on the root element fires. On iOS, the layout viewport does not resize, so ResizeObserver is useless for keyboard detection. The visualViewport API, on the other hand, fires on both platforms for keyboard and chrome changes.

Our recommendation: use visualViewport for any viewport-level adjustments (keyboard, chrome, orientation). Use ResizeObserver for element-level changes, such as when a child element resizes due to content changes or dynamic units. Do not rely on window.resize — it is deprecated and unreliable on mobile.

Orientation Changes and Multi-Window

When the device rotates, both viewports change. The visualViewport API fires a resize event, and you can check screen.orientation to determine the new angle. However, on some Android devices, the visual viewport height may be incorrect immediately after rotation due to a race condition. A 200ms delay before reading the height can avoid stale values. In multi-window environments (Samsung DeX, iPad Stage Manager), the viewport dimensions can change arbitrarily as the user resizes the window. ResizeObserver on the root element is the most reliable here, as it fires on every layout viewport change, including window resizing.

Edge Cases: Notch, Dynamic Island, and Foldables

Notches and Dynamic Islands are not viewport changes per se, but they affect the safe area. Use env(safe-area-inset-top), env(safe-area-inset-bottom), etc., to avoid placing critical content behind the notch. These CSS environment variables are supported in Safari and Chrome on devices with notches. For foldables, the viewport can split into two logical halves when the device is folded. The CSS viewport-segment-* media queries (part of the CSS Device Adaptation spec) allow you to style each segment independently, but support is currently limited to experimental flags in Chromium. For now, treat foldables as two separate viewports and use JavaScript to detect the hinge area via window.screen.isExtended and getScreenDetails API.

The Floating Keyboard on iPadOS

iPadOS allows the keyboard to float and be resized. This changes the visual viewport unpredictably. The visualViewport API still works, but the keyboard may not cover the full width. You may need to also listen to visualViewport.width changes. A common bug is that fixed elements become misaligned when the keyboard width is less than the screen width. In such cases, consider using a non-fixed layout that scrolls naturally.

Limits of the Approach and When to Abstain

Dynamic viewport management is powerful, but it has limits. First, the visualViewport API is not available in all browsers — notably, some older Android WebViews and desktop browsers. Always provide a fallback that works without it, even if it means a degraded experience (e.g., the fixed element may be covered by the keyboard). Second, excessive use of JavaScript-based viewport tracking can hurt performance. Each resize event can trigger a layout recalculation. If you have many elements depending on viewport dimensions, consider using position: sticky with bottom: env(safe-area-inset-bottom) as a simpler alternative that works for many cases without JavaScript.

Third, the new CSS viewport units (dvh, svh, lvh) are still not fully reliable across browsers. They can cause layout shifts and are difficult to debug because they depend on browser chrome state. We recommend using them only for progressive enhancement, not as the primary sizing mechanism. For critical layouts, use a combination of min-height, flexbox, and JavaScript fallbacks.

Finally, consider whether you truly need dynamic viewport management. For a simple article page, 100vh with a scrollbar is fine. The complexity is justified only for immersive interfaces like full-screen games, chat apps, media players, or interactive tools where every pixel matters. If your users rarely interact with the keyboard or rarely scroll, the extra code may introduce more bugs than it solves.

Next Steps for Your Project

  • Audit your current codebase for 100vh usage and replace with svh or dvh where appropriate, testing on real devices.
  • Implement the visualViewport API for any fixed elements near the bottom of the screen, with a debounced handler.
  • Add env(safe-area-inset-*) to your CSS for notch and status bar compatibility.
  • Set up a test matrix covering iOS Safari, Chrome Android, Samsung Internet, and iPadOS Safari.
  • Consider using a library like viewport-extra or react-viewport if you're in a framework, but be aware of bundle size and update frequency.

Share this article:

Comments (0)

No comments yet. Be the first to comment!