From 00ccd30b34dd09e1cf45e37ce96d598dc3a60665 Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Tue, 21 Jul 2026 14:48:16 +0200
Subject: [PATCH 1/9] fix: resolve issues with safe area plugin
---
.../com/getcapacitor/plugin/SystemBars.java | 95 ++++++++++++-------
cli/src/declarations.ts | 38 +++++++-
core/native-bridge.ts | 7 --
3 files changed, 94 insertions(+), 46 deletions(-)
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index b3c95916a..f9f4376b0 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -8,7 +8,6 @@
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
-import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
@@ -33,9 +32,9 @@ public class SystemBars extends Plugin {
static final String BAR_STATUS_BAR = "StatusBar";
static final String BAR_GESTURE_BAR = "NavigationBar";
- // TODO: In Cap 9, add an additional option "full"
static final String INSETS_HANDLING_CSS = "css";
static final String INSETS_HANDLING_DISABLE = "disable";
+ static final String INSETS_HANDLING_NATIVE = "native";
// https://issues.chromium.org/issues/40699457
private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140;
@@ -61,11 +60,26 @@ function capacitorSystemBarsCheckMetaViewport() {
private String currentStatusBarStyle = STYLE_DEFAULT;
private String currentGestureBarStyle = STYLE_DEFAULT;
+ // Declare variable at this scope to help prevent adding multiple listeners.
+ private WebViewListener webViewListener;
+
+ private void warnAboutUnsupportedConfigurationValues() {
+ String systemBarsInsetsHandling = bridge.getConfig().getPluginConfiguration("SystemBars").getConfigJSON().optString("insetsHandling");
+ boolean keyboardResizeOnFullScreen = bridge.getConfig().getPluginConfiguration("Keyboard").getConfigJSON().optBoolean("resizeOnFullScreen", false);
+ if (!systemBarsInsetsHandling.equals("disable") && keyboardResizeOnFullScreen) {
+ Logger.warn("SystemBars", "You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior.");
+ }
+ if (isSafeAreaPluginPresent()) {
+ Logger.warn("SystemBars", "You should uninstall `@capacitor-community/safe-area`. Having this library installed can lead to unexpected behavior.");
+ }
+ }
+
@Override
public void load() {
- getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface");
super.load();
+ warnAboutUnsupportedConfigurationValues();
+
initSystemBars();
}
@@ -73,15 +87,25 @@ public void load() {
protected void handleOnStart() {
super.handleOnStart();
- this.getBridge().addWebViewListener(
- new WebViewListener() {
- @Override
- public void onPageCommitVisible(WebView view, String url) {
- super.onPageCommitVisible(view, url);
- getBridge().getWebView().requestApplyInsets();
- }
+ boolean detectViewportFitCoverChanges = getConfig().getConfigJSON().optBoolean("detectViewportFitCoverChanges", true);
+
+ if (detectViewportFitCoverChanges) {
+ if (webViewListener == null) {
+ webViewListener = new WebViewListener() {
+ @Override
+ public void onPageCommitVisible(WebView view, String url) {
+ super.onPageCommitVisible(view, url);
+ bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
+ hasViewportCover = res.equals("true");
+
+ // Request new execution tree of `setOnApplyWindowInsetsListener`
+ bridge.getWebView().requestApplyInsets();
+ });
+ }
+ };
+ this.getBridge().addWebViewListener(webViewListener);
}
- );
+ }
}
@Override
@@ -93,11 +117,14 @@ protected void handleOnConfigurationChanged(Configuration newConfig) {
}
private void initSystemBars() {
+ // Setting this value to `true` can help prevent layout shifting if you already know that this value will end up being `true`.
+ hasViewportCover = getConfig().getConfigJSON().optBoolean("initialViewportFitCover", false);
+
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
boolean hidden = getConfig().getBoolean("hidden", false);
- String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_CSS);
- if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling)) {
+ String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_NATIVE);
+ if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) || INSETS_HANDLING_NATIVE.equals(configuredInsetsHandling)) {
insetsHandling = configuredInsetsHandling;
} else {
Logger.warn(
@@ -152,19 +179,6 @@ public void setAnimation(final PluginCall call) {
call.resolve();
}
- @JavascriptInterface
- public void onDOMReady() {
- if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
- getActivity().runOnUiThread(() -> {
- this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
- hasViewportCover = res.equals("true");
-
- getBridge().getWebView().requestApplyInsets();
- });
- });
- }
- }
-
private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
if (insets.isVisible(WindowInsetsCompat.Type.ime())) {
@@ -191,12 +205,23 @@ private void initSafeAreaCSSVariables() {
}
}
+ private static boolean isSafeAreaPluginPresent() {
+ try {
+ Class.forName("package com.getcapacitor.community.safearea.SafeAreaPlugin");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
private void initWindowInsetsListener() {
if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
return;
}
- ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
+ View view = getActivity().getWindow().getDecorView();
+
+ ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;
Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
@@ -223,15 +248,13 @@ private void initWindowInsetsListener() {
.build();
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- // We need to correct for a possible shown IME
- v.setPadding(
- systemBarsInsets.left,
- systemBarsInsets.top,
- systemBarsInsets.right,
- keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
- );
- }
+ // We need to correct for a possible shown IME
+ v.setPadding(
+ systemBarsInsets.left,
+ systemBarsInsets.top,
+ systemBarsInsets.right,
+ keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
+ );
// Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
// So we have to explicitly set insets to `0`
diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts
index 1103b6585..44cf214bf 100644
--- a/cli/src/declarations.ts
+++ b/cli/src/declarations.ts
@@ -772,19 +772,51 @@ export interface PluginsConfig {
*
* This option is only supported on Android.
*
- * `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
+ * `native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= 140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
+ *
+ * `css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
*
* `disable` = Disable CSS variables injection.
*
* @default "css"
*/
- insetsHandling?: 'css' | 'disable';
+ insetsHandling?: 'native' | 'css' | 'disable';
+
+ /**
+ * This plugin detects changes to the `viewport-fit` meta tag.
+ * This comes in handy when you do not know for sure if the content loaded into the webview will have `viewport-fit` set to `cover`.
+ * For most use cases you do not need to touch this config variable.
+ * However if you know for sure you want to always keep the `initialViewportFitCover` value unchanged,
+ * you could disable this feature by setting it to `false`.
+ * Be aware that this might result in a visually broken UI if the content loaded into the webview does not correctly handle safe area insets.
+ *
+ * This option is only supported on Android.
+ *
+ * @default true
+ */
+ detectViewportFitCoverChanges?: boolean;
+
+ /**
+ * Set an initial value for the to be detected `viewport-fit=cover`.
+ * For most apps that support edge-to-edge this value will eventually be `true`.
+ * Therefore you might want to set this value is to `true` to help prevent layout jumps and glitches.
+ * If you know (or want) the value to be `true` initially, you can set it here.
+ * The value will always end up correctly, no matter what you set here,
+ * as long as `detectViewportFitCoverChanges` is set to `true`.
+ * It only exists to help prevent layout jumps and glitches.
+ *
+ * This option is only supported on Android.
+ *
+ * @default false
+ */
+ initialViewportFitCover?: boolean;
+
/**
* The style of the text and icons of the system bars.
*
* This option is only supported on Android.
*
- * @default `DEFAULT`
+ * @default 'css
*/
style?: string;
diff --git a/core/native-bridge.ts b/core/native-bridge.ts
index 4fc028f37..50f799f81 100644
--- a/core/native-bridge.ts
+++ b/core/native-bridge.ts
@@ -377,13 +377,6 @@ const initBridge = (w: any): void => {
const platform = getPlatformId(win);
- if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
- // add DOM ready listener for System Bars
- document.addEventListener('DOMContentLoaded', function () {
- win.CapacitorSystemBarsAndroidInterface.onDOMReady();
- });
- }
-
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
From e3e5a507ec7285b307752de0f3077c7e3b259d14 Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Tue, 21 Jul 2026 14:50:28 +0200
Subject: [PATCH 2/9] style: run `npm run fmt`
---
.../com/getcapacitor/plugin/SystemBars.java | 28 +++++++++++++++----
1 file changed, 23 insertions(+), 5 deletions(-)
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index f9f4376b0..4d2c58b9e 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -64,13 +64,27 @@ function capacitorSystemBarsCheckMetaViewport() {
private WebViewListener webViewListener;
private void warnAboutUnsupportedConfigurationValues() {
- String systemBarsInsetsHandling = bridge.getConfig().getPluginConfiguration("SystemBars").getConfigJSON().optString("insetsHandling");
- boolean keyboardResizeOnFullScreen = bridge.getConfig().getPluginConfiguration("Keyboard").getConfigJSON().optBoolean("resizeOnFullScreen", false);
+ String systemBarsInsetsHandling = bridge
+ .getConfig()
+ .getPluginConfiguration("SystemBars")
+ .getConfigJSON()
+ .optString("insetsHandling");
+ boolean keyboardResizeOnFullScreen = bridge
+ .getConfig()
+ .getPluginConfiguration("Keyboard")
+ .getConfigJSON()
+ .optBoolean("resizeOnFullScreen", false);
if (!systemBarsInsetsHandling.equals("disable") && keyboardResizeOnFullScreen) {
- Logger.warn("SystemBars", "You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior.");
+ Logger.warn(
+ "SystemBars",
+ "You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior."
+ );
}
if (isSafeAreaPluginPresent()) {
- Logger.warn("SystemBars", "You should uninstall `@capacitor-community/safe-area`. Having this library installed can lead to unexpected behavior.");
+ Logger.warn(
+ "SystemBars",
+ "You should uninstall `@capacitor-community/safe-area`. Having this library installed can lead to unexpected behavior."
+ );
}
}
@@ -124,7 +138,11 @@ private void initSystemBars() {
boolean hidden = getConfig().getBoolean("hidden", false);
String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_NATIVE);
- if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) || INSETS_HANDLING_NATIVE.equals(configuredInsetsHandling)) {
+ if (
+ INSETS_HANDLING_CSS.equals(configuredInsetsHandling) ||
+ INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) ||
+ INSETS_HANDLING_NATIVE.equals(configuredInsetsHandling)
+ ) {
insetsHandling = configuredInsetsHandling;
} else {
Logger.warn(
From d58a686ace1b4b4f308e097628c14736a2c7e801 Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Tue, 21 Jul 2026 14:50:56 +0200
Subject: [PATCH 3/9] chore: run `npm run build:nativebridge`
---
android/capacitor/src/main/assets/native-bridge.js | 6 ------
ios/Capacitor/Capacitor/assets/native-bridge.js | 6 ------
2 files changed, 12 deletions(-)
diff --git a/android/capacitor/src/main/assets/native-bridge.js b/android/capacitor/src/main/assets/native-bridge.js
index f5e7cc440..20a21b4fb 100644
--- a/android/capacitor/src/main/assets/native-bridge.js
+++ b/android/capacitor/src/main/assets/native-bridge.js
@@ -367,12 +367,6 @@ var nativeBridge = (function (exports) {
}
};
const platform = getPlatformId(win);
- if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
- // add DOM ready listener for System Bars
- document.addEventListener('DOMContentLoaded', function () {
- win.CapacitorSystemBarsAndroidInterface.onDOMReady();
- });
- }
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
diff --git a/ios/Capacitor/Capacitor/assets/native-bridge.js b/ios/Capacitor/Capacitor/assets/native-bridge.js
index f5e7cc440..20a21b4fb 100644
--- a/ios/Capacitor/Capacitor/assets/native-bridge.js
+++ b/ios/Capacitor/Capacitor/assets/native-bridge.js
@@ -367,12 +367,6 @@ var nativeBridge = (function (exports) {
}
};
const platform = getPlatformId(win);
- if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
- // add DOM ready listener for System Bars
- document.addEventListener('DOMContentLoaded', function () {
- win.CapacitorSystemBarsAndroidInterface.onDOMReady();
- });
- }
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
From d59bcd174684419af1c4d9e587a9eba12582600e Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Tue, 21 Jul 2026 14:58:47 +0200
Subject: [PATCH 4/9] docs: update documentation
---
core/system-bars.md | 16 ++++------------
1 file changed, 4 insertions(+), 12 deletions(-)
diff --git a/core/system-bars.md b/core/system-bars.md
index 67275077a..a429731e4 100644
--- a/core/system-bars.md
+++ b/core/system-bars.md
@@ -24,17 +24,7 @@ The status bar visibility defaults to visible and the style defaults to
## Android Note
-Due to a [bug](https://issues.chromium.org/issues/40699457) in some older versions of Android WebView (< 140), correct safe area values are not available via the `safe-area-inset-x` CSS `env` variables. This plugin will inject the correct inset values into a new CSS variable(s) named `--safe-area-inset-x` that you can use as a fallback in your frontend styles:
-
-```css
-html {
- padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
- padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
- padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
- padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
-}
-```
-To control this behavior, use the `insetsHandling` configuration setting.
+Due to a [bug](https://issues.chromium.org/issues/40699457) in some older versions of Android WebView (< 140), correct safe area values are not available via the `safe-area-inset-x` CSS `env` variables. This plugin has two ways to workaround this. To control this behavior, use the `insetsHandling` configuration setting.
## Example
@@ -73,7 +63,9 @@ const setStatusBarAnimation = async () => {
## Configuration
| Prop | Type | Description | Default |
| ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ |
-| **`insetsHandling`** | string | Specifies how to handle problematic insets on Android. This option is only supported on Android.
`css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
`disable` = Disable CSS variables injection. | css |
+| **`insetsHandling`** | string | Specifies how to handle problematic insets on Android. This option is only supported on Android.
`native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= 140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
`css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
`disable` = Disable CSS variables injection. | css |
+| **`detectViewportFitCoverChanges`** | boolean | This plugin detects changes to the `viewport-fit` meta tag. This comes in handy when you do not know for sure if the content loaded into the webview will have `viewport-fit` set to `cover`. For most use cases you do not need to touch this config variable. However if you know for sure you want to always keep the `initialViewportFitCover` value unchanged, you could disable this feature by setting it to `false`. Be aware that this might result in a visually broken UI if the content loaded into the webview does not correctly handle safe area insets. This option is only supported on Android. | true |
+| **`initialViewportFitCover`** | boolean | Set an initial value for the to be detected `viewport-fit=cover`. For most apps that support edge-to-edge this value will eventually be `true`. Therefore you might want to set this value is to `true` to help prevent layout jumps and glitches. If you know (or want) the value to be `true` initially, you can set it here. The value will always end up correctly, no matter what you set here, as long as `detectViewportFitCoverChanges` is set to `true`. It only exists to help prevent layout jumps and glitches. This option is only supported on Android. | false |
| **`style`** | string | The style of the text and icons of the system bars. | DEFAULT |
| **`hidden`** | boolean | Hide the system bars on start. | false |
| **`animation`** | string | The type of status bar animation used when showing or hiding. This option is only supported on iOS. | FADE |
From 2e9053f4bb34bae0743b58da5548512c162e9a17 Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Tue, 21 Jul 2026 15:25:13 +0200
Subject: [PATCH 5/9] typo
---
cli/src/declarations.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts
index 44cf214bf..07f95c42d 100644
--- a/cli/src/declarations.ts
+++ b/cli/src/declarations.ts
@@ -816,7 +816,7 @@ export interface PluginsConfig {
*
* This option is only supported on Android.
*
- * @default 'css
+ * @default 'css'
*/
style?: string;
From a08187a05b04bf417181256f26561db4329c3e31 Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Wed, 22 Jul 2026 07:21:05 +0200
Subject: [PATCH 6/9] fix: make `InsetsHandling.css` work correctly
---
.../com/getcapacitor/plugin/SystemBars.java | 59 +++++++------------
1 file changed, 20 insertions(+), 39 deletions(-)
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index 4d2c58b9e..b8787cb54 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -4,7 +4,6 @@
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.os.Build;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
@@ -153,7 +152,6 @@ private void initSystemBars() {
}
initWindowInsetsListener();
- initSafeAreaCSSVariables();
getBridge().executeOnMainThread(() -> {
setStyle(style, "");
@@ -197,32 +195,6 @@ public void setAnimation(final PluginCall call) {
call.resolve();
}
- private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
- Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
- if (insets.isVisible(WindowInsetsCompat.Type.ime())) {
- return Insets.of(safeArea.left, safeArea.top, safeArea.right, 0);
- }
- return Insets.of(safeArea.left, safeArea.top, safeArea.right, safeArea.bottom);
- }
-
- private void initSafeAreaCSSVariables() {
- if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
- WindowInsetsCompat insets;
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- View v = (View) this.getBridge().getWebView().getParent();
- insets = ViewCompat.getRootWindowInsets(v);
- } else {
- insets = WindowInsetsCompat.CONSUMED;
- }
-
- if (insets != null) {
- Insets safeAreaInsets = calcSafeAreaInsets(insets);
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
- }
- }
- }
-
private static boolean isSafeAreaPluginPresent() {
try {
Class.forName("package com.getcapacitor.community.safearea.SafeAreaPlugin");
@@ -250,10 +222,7 @@ private void initWindowInsetsListener() {
// We need to correct for a possible shown IME
v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);
- Insets safeAreaInsets = calcSafeAreaInsets(insets);
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
-
- return new WindowInsetsCompat.Builder(insets)
+ WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets)
.setInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
Insets.of(
@@ -264,6 +233,10 @@ private void initWindowInsetsListener() {
)
)
.build();
+
+ injectSafeAreaCSS(newInsets);
+
+ return newInsets;
}
// We need to correct for a possible shown IME
@@ -281,20 +254,28 @@ private void initWindowInsetsListener() {
.setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0))
.build();
- Insets safeAreaInsets = calcSafeAreaInsets(newInsets);
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
+ injectSafeAreaCSS(newInsets);
return newInsets;
});
}
- private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
+ private void injectSafeAreaCSS(WindowInsetsCompat insets) {
+ if (!INSETS_HANDLING_CSS.equals(insetsHandling)) {
+ return;
+ }
+
+ Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
+ boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
+
// Convert pixels to density-independent pixels
float density = getActivity().getResources().getDisplayMetrics().density;
- float topPx = top / density;
- float rightPx = right / density;
- float bottomPx = bottom / density;
- float leftPx = left / density;
+ float topPx = systemBarsInsets.top / density;
+ float rightPx = systemBarsInsets.right / density;
+ // For native insets the value gets automatically corrected when the IME is visible (in newer WebView versions),
+ // but for these injected values we have to handle that manually (for all WebView versions).
+ float bottomPx = (keyboardVisible ? 0 : systemBarsInsets.bottom) / density;
+ float leftPx = systemBarsInsets.left / density;
// Execute JavaScript to inject the CSS
getBridge().executeOnMainThread(() -> {
From b7e9818d8cd4bd2d5237925b041d66bf304c27bd Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Mon, 27 Jul 2026 08:37:23 +0200
Subject: [PATCH 7/9] refactor: improve warning about unsupported values
---
.../com/getcapacitor/plugin/SystemBars.java | 33 +++----------------
1 file changed, 5 insertions(+), 28 deletions(-)
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index b8787cb54..644c7b0ff 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -63,36 +63,20 @@ function capacitorSystemBarsCheckMetaViewport() {
private WebViewListener webViewListener;
private void warnAboutUnsupportedConfigurationValues() {
- String systemBarsInsetsHandling = bridge
- .getConfig()
- .getPluginConfiguration("SystemBars")
- .getConfigJSON()
- .optString("insetsHandling");
- boolean keyboardResizeOnFullScreen = bridge
- .getConfig()
- .getPluginConfiguration("Keyboard")
- .getConfigJSON()
- .optBoolean("resizeOnFullScreen", false);
- if (!systemBarsInsetsHandling.equals("disable") && keyboardResizeOnFullScreen) {
+ boolean keyboardResizeOnFullScreen = bridge.getConfig().getPluginConfiguration("Keyboard").getBoolean("resizeOnFullScreen", false);
+
+ if (!INSETS_HANDLING_DISABLE.equals(insetsHandling) && keyboardResizeOnFullScreen) {
Logger.warn(
"SystemBars",
"You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior."
);
}
- if (isSafeAreaPluginPresent()) {
- Logger.warn(
- "SystemBars",
- "You should uninstall `@capacitor-community/safe-area`. Having this library installed can lead to unexpected behavior."
- );
- }
}
@Override
public void load() {
super.load();
- warnAboutUnsupportedConfigurationValues();
-
initSystemBars();
}
@@ -151,6 +135,8 @@ private void initSystemBars() {
insetsHandling = INSETS_HANDLING_CSS;
}
+ warnAboutUnsupportedConfigurationValues();
+
initWindowInsetsListener();
getBridge().executeOnMainThread(() -> {
@@ -195,15 +181,6 @@ public void setAnimation(final PluginCall call) {
call.resolve();
}
- private static boolean isSafeAreaPluginPresent() {
- try {
- Class.forName("package com.getcapacitor.community.safearea.SafeAreaPlugin");
- return true;
- } catch (ClassNotFoundException e) {
- return false;
- }
- }
-
private void initWindowInsetsListener() {
if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
return;
From 9b35343f3c5aa91d0f1b2446470e8a872120ef85 Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Mon, 27 Jul 2026 08:38:38 +0200
Subject: [PATCH 8/9] feat: remove `detectViewportFitCoverChanges` and rename
`initialViewportFitValueHint`
---
.../com/getcapacitor/plugin/SystemBars.java | 42 ++++++++++---------
cli/src/declarations.ts | 32 +++++---------
core/system-bars.md | 5 +--
3 files changed, 34 insertions(+), 45 deletions(-)
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index 644c7b0ff..f8dd05cbe 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -84,24 +84,24 @@ public void load() {
protected void handleOnStart() {
super.handleOnStart();
- boolean detectViewportFitCoverChanges = getConfig().getConfigJSON().optBoolean("detectViewportFitCoverChanges", true);
-
- if (detectViewportFitCoverChanges) {
- if (webViewListener == null) {
- webViewListener = new WebViewListener() {
- @Override
- public void onPageCommitVisible(WebView view, String url) {
- super.onPageCommitVisible(view, url);
- bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
- hasViewportCover = res.equals("true");
-
- // Request new execution tree of `setOnApplyWindowInsetsListener`
- bridge.getWebView().requestApplyInsets();
- });
- }
- };
- this.getBridge().addWebViewListener(webViewListener);
- }
+ if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
+ return;
+ }
+
+ if (webViewListener == null) {
+ webViewListener = new WebViewListener() {
+ @Override
+ public void onPageCommitVisible(WebView view, String url) {
+ super.onPageCommitVisible(view, url);
+ bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
+ hasViewportCover = res.equals("true");
+
+ // Request new execution tree of `setOnApplyWindowInsetsListener`
+ bridge.getWebView().requestApplyInsets();
+ });
+ }
+ };
+ this.getBridge().addWebViewListener(webViewListener);
}
}
@@ -114,8 +114,10 @@ protected void handleOnConfigurationChanged(Configuration newConfig) {
}
private void initSystemBars() {
- // Setting this value to `true` can help prevent layout shifting if you already know that this value will end up being `true`.
- hasViewportCover = getConfig().getConfigJSON().optBoolean("initialViewportFitCover", false);
+ // If you already know what the value of the `viewport-fit=` meta tag is going to be,
+ // passing it here through `initialViewportFitValueHint` can help prevent layout shifting.
+ String configuredInitialViewportFitValueHint = getConfig().getString("initialViewportFitValueHint", "");
+ hasViewportCover = "cover".equals(configuredInitialViewportFitValueHint);
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
boolean hidden = getConfig().getBoolean("hidden", false);
diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts
index 07f95c42d..a4855ce28 100644
--- a/cli/src/declarations.ts
+++ b/cli/src/declarations.ts
@@ -772,44 +772,32 @@ export interface PluginsConfig {
*
* This option is only supported on Android.
*
- * `native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= 140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
+ * `native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
*
* `css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
*
- * `disable` = Disable CSS variables injection.
+ * `disable` = (not recommended) Disable safe area insets handling completely.
+ * This shifts the responsibility from Capacitor to your own code to handle the insets.
+ * Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.
*
* @default "css"
*/
insetsHandling?: 'native' | 'css' | 'disable';
/**
- * This plugin detects changes to the `viewport-fit` meta tag.
- * This comes in handy when you do not know for sure if the content loaded into the webview will have `viewport-fit` set to `cover`.
- * For most use cases you do not need to touch this config variable.
- * However if you know for sure you want to always keep the `initialViewportFitCover` value unchanged,
- * you could disable this feature by setting it to `false`.
- * Be aware that this might result in a visually broken UI if the content loaded into the webview does not correctly handle safe area insets.
- *
- * This option is only supported on Android.
- *
- * @default true
- */
- detectViewportFitCoverChanges?: boolean;
-
- /**
- * Set an initial value for the to be detected `viewport-fit=cover`.
- * For most apps that support edge-to-edge this value will eventually be `true`.
- * Therefore you might want to set this value is to `true` to help prevent layout jumps and glitches.
- * If you know (or want) the value to be `true` initially, you can set it here.
+ * Set an initial value for the to be detected `viewport-fit=` meta tag value.
+ * For most apps that support edge-to-edge this value will eventually be `cover`.
+ * Therefore you might want to set this value is to `cover` to help prevent layout jumps and glitches.
+ * If you know the value to be `cover` initially, you can set it here.
* The value will always end up correctly, no matter what you set here,
- * as long as `detectViewportFitCoverChanges` is set to `true`.
+ * as long as `insetsHandling` is set to `native` or `css`.
* It only exists to help prevent layout jumps and glitches.
*
* This option is only supported on Android.
*
* @default false
*/
- initialViewportFitCover?: boolean;
+ initialViewportFitValueHint?: 'auto' | 'contain' | 'cover';
/**
* The style of the text and icons of the system bars.
diff --git a/core/system-bars.md b/core/system-bars.md
index a429731e4..009520ac0 100644
--- a/core/system-bars.md
+++ b/core/system-bars.md
@@ -63,9 +63,8 @@ const setStatusBarAnimation = async () => {
## Configuration
| Prop | Type | Description | Default |
| ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ |
-| **`insetsHandling`** | string | Specifies how to handle problematic insets on Android. This option is only supported on Android.
`native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= 140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
`css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
`disable` = Disable CSS variables injection. | css |
-| **`detectViewportFitCoverChanges`** | boolean | This plugin detects changes to the `viewport-fit` meta tag. This comes in handy when you do not know for sure if the content loaded into the webview will have `viewport-fit` set to `cover`. For most use cases you do not need to touch this config variable. However if you know for sure you want to always keep the `initialViewportFitCover` value unchanged, you could disable this feature by setting it to `false`. Be aware that this might result in a visually broken UI if the content loaded into the webview does not correctly handle safe area insets. This option is only supported on Android. | true |
-| **`initialViewportFitCover`** | boolean | Set an initial value for the to be detected `viewport-fit=cover`. For most apps that support edge-to-edge this value will eventually be `true`. Therefore you might want to set this value is to `true` to help prevent layout jumps and glitches. If you know (or want) the value to be `true` initially, you can set it here. The value will always end up correctly, no matter what you set here, as long as `detectViewportFitCoverChanges` is set to `true`. It only exists to help prevent layout jumps and glitches. This option is only supported on Android. | false |
+| **`insetsHandling`** | string | Specifies how to handle problematic insets on Android.
This option is only supported on Android.
`native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
`css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
`disable` = (not recommended) Disable safe area insets handling completely.
This shifts the responsibility from Capacitor to your own code to handle the insets.
Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.
| css |
+| **`initialViewportFitValueHint`** | string | Set an initial value for the to be detected `viewport-fit=` meta tag value.
For most apps that support edge-to-edge this value will eventually be `cover`.
Therefore you might want to set this value is to `cover` to help prevent layout jumps and glitches.
If you know the value to be `cover` initially, you can set it here.
The value will always end up correctly, no matter what you set here,
as long as `insetsHandling` is set to `native` or `css`.
It only exists to help prevent layout jumps and glitches.
This option is only supported on Android. | false |
| **`style`** | string | The style of the text and icons of the system bars. | DEFAULT |
| **`hidden`** | boolean | Hide the system bars on start. | false |
| **`animation`** | string | The type of status bar animation used when showing or hiding. This option is only supported on iOS. | FADE |
From 4d9996b51b0dcc449a08b70873625fc6d9051121 Mon Sep 17 00:00:00 2001
From: tafelnl <35837839+tafelnl@users.noreply.github.com>
Date: Tue, 4 Aug 2026 16:23:34 +0200
Subject: [PATCH 9/9] fix: whoops
---
.../src/main/java/com/getcapacitor/plugin/SystemBars.java | 2 +-
cli/src/declarations.ts | 4 ++--
core/system-bars.md | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index f8dd05cbe..8b9de8849 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -122,7 +122,7 @@ private void initSystemBars() {
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
boolean hidden = getConfig().getBoolean("hidden", false);
- String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_NATIVE);
+ String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_CSS);
if (
INSETS_HANDLING_CSS.equals(configuredInsetsHandling) ||
INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) ||
diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts
index a4855ce28..d36e23f48 100644
--- a/cli/src/declarations.ts
+++ b/cli/src/declarations.ts
@@ -787,7 +787,7 @@ export interface PluginsConfig {
/**
* Set an initial value for the to be detected `viewport-fit=` meta tag value.
* For most apps that support edge-to-edge this value will eventually be `cover`.
- * Therefore you might want to set this value is to `cover` to help prevent layout jumps and glitches.
+ * Therefore you might want to set this value to `cover` to help prevent layout jumps and glitches.
* If you know the value to be `cover` initially, you can set it here.
* The value will always end up correctly, no matter what you set here,
* as long as `insetsHandling` is set to `native` or `css`.
@@ -795,7 +795,7 @@ export interface PluginsConfig {
*
* This option is only supported on Android.
*
- * @default false
+ * @default undefined
*/
initialViewportFitValueHint?: 'auto' | 'contain' | 'cover';
diff --git a/core/system-bars.md b/core/system-bars.md
index 009520ac0..fb6077f5c 100644
--- a/core/system-bars.md
+++ b/core/system-bars.md
@@ -64,7 +64,7 @@ const setStatusBarAnimation = async () => {
| Prop | Type | Description | Default |
| ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ |
| **`insetsHandling`** | string | Specifies how to handle problematic insets on Android.
This option is only supported on Android.
`native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
`css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
`disable` = (not recommended) Disable safe area insets handling completely.
This shifts the responsibility from Capacitor to your own code to handle the insets.
Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.
| css |
-| **`initialViewportFitValueHint`** | string | Set an initial value for the to be detected `viewport-fit=` meta tag value.
For most apps that support edge-to-edge this value will eventually be `cover`.
Therefore you might want to set this value is to `cover` to help prevent layout jumps and glitches.
If you know the value to be `cover` initially, you can set it here.
The value will always end up correctly, no matter what you set here,
as long as `insetsHandling` is set to `native` or `css`.
It only exists to help prevent layout jumps and glitches.
This option is only supported on Android. | false |
+| **`initialViewportFitValueHint`** | string | Set an initial value for the to be detected `viewport-fit=` meta tag value.
For most apps that support edge-to-edge this value will eventually be `cover`.
Therefore you might want to set this value to `cover` to help prevent layout jumps and glitches.
If you know the value to be `cover` initially, you can set it here.
The value will always end up correctly, no matter what you set here,
as long as `insetsHandling` is set to `native` or `css`.
It only exists to help prevent layout jumps and glitches.
This option is only supported on Android. | false |
| **`style`** | string | The style of the text and icons of the system bars. | DEFAULT |
| **`hidden`** | boolean | Hide the system bars on start. | false |
| **`animation`** | string | The type of status bar animation used when showing or hiding. This option is only supported on iOS. | FADE |