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 |