Skip to content

Bug(android): SystemBars breaks edge-to-edge and does not expose top inset on Android WebView < 140 #8651

Open

Description

@adrianofalleti98

Capacitor Version

💊 Capacitor Doctor 💊

Latest Dependencies:

@capacitor/cli: 8.5.3
@capacitor/core: 8.5.3
@capacitor/android: 8.5.3
@capacitor/ios: 8.5.3

Installed Dependencies:

@capacitor/ios: not installed
@capacitor/cli: 8.5.3
@capacitor/android: 8.5.3
@capacitor/core: 8.5.3

[success] Android looking great! 👌

Other API Details

- @capacitor/core, @capacitor/android, @capacitor/cli: 8.5.3
- minSdkVersion 24, compileSdkVersion 36, targetSdkVersion 36
- androidx.activity 1.11.0
- EdgeToEdge.enable(this) called in MainActivity.onCreate(), right after super.onCreate()
- <meta name="viewport" content="viewport-fit=cover, ...">
- No SystemBars configuration (default insetsHandling: "css")
- Tested on Android 10 → 17 (API 29 → 37), emulators and physical devices, Android System WebView 91 → 154

Platforms Affected

  • iOS
  • Android
  • Web

Current Behavior

On devices with Android System WebView < 140, even with viewport-fit=cover and EdgeToEdge.enable(this) in MainActivity:

  • the WebView content is not drawn edge-to-edge: it is laid out between the status bar and the navigation bar;
  • the page receives no top inset: env(safe-area-inset-top) = 0px and the injected --safe-area-inset-top = 0px.

With WebView ≥ 140 everything works as expected.

Android API WebView env(safe-area-inset-top) --safe-area-inset-top Result
10 29 91 0px 0px ❌ not edge-to-edge
11 30 91 0px 0px ❌ not edge-to-edge
12 31 91 0px 0px ❌ not edge-to-edge
12L 32 120 0px 0px ❌ not edge-to-edge
13 33 154 33px 33px ✅
14 34 151 24px 24px ✅
15 35 154 55px 54px ✅
16 36 153 36px 35px ✅
17 37 153 55px 54px ✅

Screenshots and details for each version are in the reproduction repository README.

Cause, in SystemBars.java → initWindowInsetsListener():

boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;

When the WebView is < 140, shouldPassthroughInsets is false even if the page declares viewport-fit=cover, so the listener:

  1. applies the system bar insets as padding on the decor view (v.setPadding(left, top, right, bottom)), which undoes EdgeToEdge.enable();
  2. passes zeroed insets to the WebView and to injectSafeAreaCSS(), so --safe-area-inset-top is 0px.

Expected Behavior

When the page declares viewport-fit=cover, the WebView should be drawn edge-to-edge on every Android/WebView version, and --safe-area-inset-top should contain the status bar height, so that it can be used where env(safe-area-inset-top) is not reliable (WebView < 140).

Project Reproduction

https://github.com/adrianofalleti98/capacitor-systembars-repro

Additional Information

Possible fix

Decide passthrough only on viewport-fit=cover, and since on WebView < 140 the env(safe-area-inset-*) values are not reliable, the insets are always injected as CSS variables

 ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
-    boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;
+    boolean shouldPassthroughInsets = hasViewportCover;

     ...

With this change the app must enable edge-to-edge itself (e.g. EdgeToEdge.enable(this) in MainActivity.onCreate()), otherwise on API ≤ 34 the content would not be laid out behind the system bars because insets are sent to the child.

Activity

  1. dpa99c commented on Oct 29, 2026

    @dpa99c

    Additional data from the other side of the threshold: in our app the passthrough path is also broken on Android System WebView 140–143. env(safe-area-inset-*) is 0px up to and including 143.0.7499.3, and correct from 144.0.7559.133.

    Environment

    - @capacitor/android 8.3.1 (@capacitor/core, @capacitor/cli 8.5.0)
    - minSdkVersion 30, compileSdkVersion 37, targetSdkVersion 37
    - androidx.activity 1.11.0
    - EdgeToEdge.enable(...) called in MainActivity.onCreate(), before super.onCreate()
    - <meta name="viewport" content="..., viewport-fit=cover">
    - SystemBars insetsHandling: default ("css")
    - Pixel 9 Pro emulator, Android 16 (API 36)
        - WebView changed by installing Android System WebView APKs on the same image

    The WEBVIEW_VERSION_WITH_SAFE_AREA_FIX check is the same in 8.5.3 (as quoted above), so this should still apply on the latest version.

    Results

    Same emulator and same app build; only the WebView version changes. Every row takes the passthrough path (getWebViewMajorVersion() >= 140).

    Android API WebView env(safe-area-inset-top) env(safe-area-inset-bottom) Result
    16 36 143.0.7499.3 0px 0px ❌ content under status bar and navigation bar
    16 36 144.0.7559.133 52px 24px ✅
    16 36 145.0.7632.45 52px 24px ✅
    16 36 146.0.7680.178 52px 24px ✅
    16 36 148.0.7778.217 52px 24px ✅

    Values were read with getComputedStyle(document.documentElement).getPropertyValue(...) on custom properties defined as env(safe-area-inset-top, 0px) / env(safe-area-inset-bottom, 0px).

    On 143.0.7499.3 the WebView is full screen, so Capacitor has removed the padding but the page gets no insets:

    innerHeight
    > 952
    screen.height
    > 952
    visualViewport.height
    > 952
    visualViewport.offsetTop
    > 0

    The same symptom appears on a physical Galaxy S26 (Android 16, launch firmware, WebView 140.0.7339.207); env() was not measured there. Physical devices on WebView 157/158 (Android 11, 14 and 17) render correctly.

    Note: I can't report Capacitor's injected --safe-area-inset-top/-bottom for these runs, because our app overwrites those two variables from env(). The injected --safe-area-inset-left/-right were present inline, so injectSafeAreaCSS() did run. Apps that use only the injected variables may therefore not see this on API 35+. On API ≤ 34 nothing is injected (Build.VERSION.SDK_INT >= VANILLA_ICE_CREAM check), so env() is the only source there.

    Cause

    This matches the inset change documented by Chromium in Android WebView & Insets:

    In M136, WebView added support for displayCutout() insets and systemBars() insets. These were forwarded to the web content via the safe-area-insets CSS values. Due to compatibility issues, we only supported passing these inset values to the web content when the WebView was fullscreen.

    In M144, we expanded inset support to all WebViews regardless of whether they are displaying fullscreen or not.

    So on 140–143, whether env(safe-area-inset-*) is populated depends on WebView's own fullscreen check. Our WebView fills the window (see above) but evidently doesn't pass that check; I haven't dug into why. Apps that do pass it would get correct values from 140, which may be why 140 looked like the right threshold. From 144 the check no longer applies, which matches the boundary in the table.

    Possible fix

    Raise WEBVIEW_VERSION_WITH_SAFE_AREA_FIX from 140 to 144, the version from which Chromium forwards the insets to all WebViews. On 140–143 this puts the WebView on the padding path, which lays out correctly on those versions.

    -    private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140;
    +    private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 144;

    On the fix proposed above (passthrough based only on viewport-fit=cover): on 140–143 it would depend on the injected CSS variables, as on < 140. In 8.3.1 those are injected only on API 35+. I haven't checked whether 8.5.x changed that.

    For reference, we've worked around it in the app by padding android.R.id.content with the systemBars() | displayCutout() insets and passing cleared insets on (insets.inset(...), not CONSUMED), with insetsHandling: "disable". With that, layout no longer depends on the WebView version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions