Repository navigation
Bug(android): SystemBars breaks edge-to-edge and does not expose top inset on Android WebView < 140 #8651
Description
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 → 154Platforms 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)=0pxand 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:
- applies the system bar insets as padding on the decor view (
v.setPadding(left, top, right, bottom)), which undoesEdgeToEdge.enable(); - passes zeroed insets to the WebView and to
injectSafeAreaCSS(), so--safe-area-inset-topis0px.
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
dpa99c commentedon Oct 29, 2026
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-*)is0pxup to and including 143.0.7499.3, and correct from 144.0.7559.133.Environment
The
WEBVIEW_VERSION_WITH_SAFE_AREA_FIXcheck 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).env(safe-area-inset-top)env(safe-area-inset-bottom)Values were read with
getComputedStyle(document.documentElement).getPropertyValue(...)on custom properties defined asenv(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:
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/-bottomfor these runs, because our app overwrites those two variables fromenv(). The injected--safe-area-inset-left/-rightwere present inline, soinjectSafeAreaCSS()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_CREAMcheck), soenv()is the only source there.Cause
This matches the inset change documented by Chromium in Android WebView & Insets:
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_FIXfrom 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.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.contentwith thesystemBars() | displayCutout()insets and passing cleared insets on (insets.inset(...), notCONSUMED), withinsetsHandling: "disable". With that, layout no longer depends on the WebView version.