How to Fix iOS PWA White Gap Below Bottom Navigation

Summary

A production issue was identified in an iOS Progressive Web App (PWA) where a white gap appeared at the bottom of the screen, below the bottom navigation bar. Despite using viewport-fit=cover and 100dvh, the layout failed to extend into the safe area inset (the home indicator zone). This resulted in a broken user experience that looked unpolished and non-native.

Root Cause

The issue stems from a fundamental misunderstanding of how WebKit calculates dynamic viewport units (dvh, svh, lvh) when a PWA is running in standalone mode with viewport-fit=cover.

  • Viewport Calculation Discrepancy: In standalone PWA mode, iOS Safari treats the “viewport” as the area above the safe area insets rather than the physical screen dimensions.
  • The 100dvh Trap: While 100dvh is designed to account for browser UI (like URL bars), in a standalone PWA, WebKit calculates 100dvh to be the height of the content area excluding the bottom safe area.
  • The Flexbox Conflict: Because the layout container was set to h-[100dvh], the entire application—including the bottom navigation—was being constrained to a box that physically ended before the screen edge.
  • Safe Area Misuse: Using padding-bottom: env(safe-area-inset-bottom) on a child element inside a container that is already too short does nothing to fill the physical gap; it only increases the internal size of a container that is already undersized.

Why This Happens in Real Systems

This happens because browser implementations of viewport units are not standardized across environments.

  • Contextual Scaling: A “viewport” is not a physical constant; it is a logical construct defined by the rendering engine. In a standard browser, the viewport includes the area under the URL bar. In a PWA, the engine attempts to protect the “safe areas” to prevent accidental interactions with the home indicator.
  • Abstraction Layers: Frameworks like Nuxt or React add multiple layers of <div> wrappers. When you try to fix a height issue at the top level, these intermediate wrappers often lack the necessary height definitions to pass the “full height” property down the DOM tree.
  • The “Safe Area” Paradox: viewport-fit=cover tells the browser “I want to use the whole screen,” but WebKit’s calculation for 100% or 100dvh often defaults to the “safe” area to prevent content from being unreadable, creating a conflict between the developer’s intent and the browser’s safety logic.

Real-World Impact

  • Perceived Quality: A white gap at the bottom of a mobile app is a “tell” that the application is a website and not a native app, destroying user trust and perceived premium quality.
  • UX Friction: If the background doesn’t extend to the edge, the “app-like” feel is lost, and the interface feels disconnected from the hardware.
  • Layout Inconsistency: It creates “dead zones” where the UI appears to end prematurely, often leading users to believe the app has crashed or failed to load content.

Example or Code

To fix this, we must stop relying on dvh for the root container and instead use a combination of fixed positioning and explicit viewport filling that ignores the safe area height calculation.

/* The Fix: Force the root to ignore the safe-area-height calculation */
html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  /* Use height: 100% instead of dvh to force the browser 
     to respect the physical viewport in standalone mode */
  height: 100%; 
  overflow: hidden;
}

/* The Layout Wrapper */
.app-container {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
}

/* The Content Area */
.main-content {
  flex: 1;
  overflow-y: auto;
  /* Ensure content doesn't get hidden under the nav */
  padding-bottom: env(safe-area-inset-bottom);
}

/* The Bottom Nav */
.bottom-nav {
  flex-shrink: 0;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(12px);
  /* This padding ensures the background color 
     extends INTO the safe area */
  padding-bottom: env(safe-area-inset-bottom);
  padding-top: 8px;
}

How Senior Engineers Fix It

A senior engineer approaches this by breaking the dependency on dynamic viewport units and moving toward absolute/fixed positioning for the app shell.

  1. Shell-First Architecture: Instead of a flow-based layout (where elements push each other), use a fixed-position shell. The shell is pinned to top: 0, bottom: 0, left: 0, right: 0. This forces the container to match the physical screen bounds.
  2. Decoupling Content from Container: We treat the main content area as a scrollable sub-window. This prevents the “body scroll” issue that breaks fixed headers.
  3. Safe Area Injection: We don’t use safe areas to resize the container; we use them to pad the content inside a container that already occupies the full screen.
  4. Testing Hardware Realities: We don’t just test on a simulator; we test on actual “notch” and “home indicator” devices to see how the env() variables actually behave in standalone vs browser modes.

Why Juniors Miss It

  • Over-reliance on Modern CSS: Juniors often assume 100dvh is a “silver bullet” for all mobile height issues. They don’t realize that “dynamic” units are calculated based on the browser’s interpretation of the viewport, which changes based on the display mode (PWA vs Safari).
  • Ignoring the “Shell” Concept: Juniors tend to build layouts as a single flowing document. When the document height is wrong, the whole app breaks. Seniors build an app shell that is independent of the content inside it.
  • Misunderstanding viewport-fit=cover: They assume viewport-fit=cover automatically fixes all layout issues, not realizing it only permits the layout to exist in the safe area—it does not automatically scale your CSS units to fill it.

Leave a Comment