Skip to content

Translucent ion-header/ion-toolbar measures --ion-safe-area-top exactly once and never re-measures, breaking on platforms where the real value arrives asynchronously (Android edge-to-edge) #31337

Description

@schallm

Title: Translucent ion-header/ion-toolbar measures --ion-safe-area-top exactly once and never re-measures, breaking on platforms where the real value arrives asynchronously (Android edge-to-edge)

Prerequisites

Ionic Info

Ionic:
   Ionic CLI                       : (not relevant, using @ionic/angular directly)
   @ionic/angular                  : 8.8.16

Capacitor:
   @capacitor/android              : 8.5.0
   @capacitor/core                 : 8.5.0

Platform: Android (Capacitor), targetSdk 35+/36, edge-to-edge (mandatory on this OS version, no opt-out).

Current Behavior

A translucent <ion-header [translucent]="true"> with a matching <ion-content [fullscreen]="true"> measures the app's --ion-safe-area-top CSS custom property exactly once, at component initialization, and caches whatever value it read at that moment. If the real value only becomes known after that initial measurement — which is unavoidable on Android, where the true status-bar inset is only available asynchronously via a native WindowInsets callback dispatched after Activity.onCreate() — the header permanently renders as if the safe area were 0, even once the CSS variable is later updated to the correct value.

Confirmed via a runtime probe logging both the CSS variable and the toolbar's measured position over time:

t0:      --ion-safe-area-top=0px   toolbarTop=n/a
t100ms:  --ion-safe-area-top=24px  toolbarTop=n/a
t500ms:  --ion-safe-area-top=24px  toolbarTop=0
t1500ms: --ion-safe-area-top=24px  toolbarTop=0
t3000ms: --ion-safe-area-top=24px  toolbarTop=0

The CSS variable is correctly updated to 24px by ~100ms and stays correct — but the toolbar's own layout never reflects it, at any point afterward, including 3 seconds later.

Expected Behavior

The header should react to changes in --ion-safe-area-top (and the other --ion-safe-area-* variables) after initialization — e.g. via a MutationObserver on the CSS custom property, or by re-measuring on window resize/orientation events, rather than caching a one-time JS measurement. This would make translucent headers correctly handle any platform/runtime where the true safe-area value isn't synchronously available at component init — which, going forward, includes Android, now that edge-to-edge is OS-mandatory there (Android 15+) and Capacitor's WebView doesn't populate env(safe-area-inset-*) for the general status bar (only for display cutouts — a separate, narrower case that iOS's WKWebView doesn't distinguish this way).

Steps to Reproduce

  1. Capacitor Android app, targetSdk 35+.
  2. Native side: apply the real status-bar inset to the WebView late (e.g. via a WindowInsets listener registered on the Activity's decor view, since — separately — listening directly on the WebView itself receives all-zero insets on Capacitor; see companion issue filed against capacitor-android), pushing the correct value into --ion-safe-area-top via document.documentElement.style.setProperty(...) once it's known.
  3. Observe: even though the CSS variable is now correct, any [translucent]="true" ion-header present at that point continues to render as if it were 0 — content still appears under the status bar.
  4. A plain (non-translucent) header, or a completely fresh page navigated to after the value is corrected, is unaffected — only headers that existed at/before the mismeasurement stay wrong, consistent with a cached one-time read rather than a live calculation.

Code Reproduction Link

Happy to put together a minimal Capacitor+Ionic Angular repro if useful — the behavior above was observed in a real app, not yet isolated into a standalone reproduction repo.

Ionic Framework Info

Framework: Angular (@ionic/angular 8.8.16)
Capacitor: 8.5.0
Platform: Android, targetSdk 36

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions