capacitor
capacitor copied to clipboard
[Bug]: Android Edge-to-edge handling on devices < API 35 broken
Capacitor Version
💊 Capacitor Doctor 💊
Latest Dependencies:
@capacitor/cli: 7.2.0
@capacitor/core: 7.2.0
@capacitor/android: 7.2.0
@capacitor/ios: 7.2.0
Installed Dependencies:
@capacitor/cli: 7.2.0
@capacitor/core: 7.2.0
@capacitor/ios: 7.2.0
@capacitor/android: 7.2.0
Other API Details
npm: `10.9.2`
node: `v22.13.0`
pod: `1.16.2`
Platforms Affected
- [ ] iOS
- [x] Android
- [ ] Web
Current Behavior
After the upgrade to v.7.2.0 which includes all fixes related to the new Android 15 Edge-to-edge enforcement (#7885 and #7919) the edge-to-edge handling still seems to have some broken cases.
I tried all values of the new adjustMarginsForEdgeToEdge setting on two emulators, a Pixel 6 with API 33 and a Pixel 9 with API 35:
Without adjustMarginsForEdgeToEdge (default 'disable'):
- Pixel 9 API 35:
ion-safe-area-top: 55px✅ion-safe-area-bottom: 0px❌ --> this is related to a known bug in the webview where the safe area does not include system bars (see here)
- Pixel 6 API 33:
ion-safe-area-top: 0px❌ --> part of the content is hidden behind the status barion-safe-area-bottom: 0px❌ --> part of the content is hidden behind the status bar
With adjustMarginsForEdgeToEdge: 'auto':
- Pixel 9 API 35:
ion-safe-area-top: 0px✅ --> margin is correctly set on the webview (content cannot reach top edge)ion-safe-area-bottom: 0px✅ --> margin is correctly set on the webview (content cannot reach bottom edge)
- Pixel 6 API 33:
ion-safe-area-top: 0px❌ --> part of the content is hidden behind the status barion-safe-area-bottom: 0px❌ --> part of the content is hidden behind the status bar
With With adjustMarginsForEdgeToEdge: 'force':
- Pixel 9 API 35:
ion-safe-area-top: 0px✅ --> margin is correctly set on the webview (content cannot reach top edge)ion-safe-area-bottom: 0px✅ --> margin is correctly set on the webview (content cannot reach bottom edge)
- Pixel 6 API 33:
ion-safe-area-top: 0px✅ --> margin is set on the webview (content cannot reach top edge)ion-safe-area-bottom: 0px✅ --> margin is set on the webview (content cannot reach bottom edge)
Expected Behavior
I would like to use adjustMarginsForEdgeToEdge: 'disable' to use all the available space for devices with API 35, but this causes content on older devices to be cut off. If I see it correctly, it would be required to set the layout margins for all devices running Android < 35 to keep the functionality of Capacitor 6.x, but still support the new and future way of Android.
Adding the following code to MainActivity.java fixes this issue for me:
private void edgeToEdgeHandler() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
ViewCompat.setOnApplyWindowInsetsListener(bridge.getWebView(), (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
mlp.leftMargin = insets.left;
mlp.bottomMargin = insets.bottom;
mlp.rightMargin = insets.right;
mlp.topMargin = insets.top;
v.setLayoutParams(mlp);
// Don't pass window insets to children
return WindowInsetsCompat.CONSUMED;
});
}
}
Maybe it's reasonable to add this code to capacitor itself. This way upgrading from version 6 would be seamless.
Project Reproduction
any Ionic starter project. Created one here and added the suggested fix (currently commented out)
Additional Information
No response
This issue needs more information before it can be addressed. In particular, the reporter needs to provide a minimal sample app that demonstrates the issue. If no sample app is provided within 15 days, the issue will be closed. Please see the Contributing Guide for how to create a Sample App. Thanks! Ionitron 💙
Added a sample repository with the Ionic tabs starter project. Also added the code of the suggested fix (commented out in MainActivity.java)
I went down the rabbit hole a bit further and came up with the following overall solution.
For api levels < 35, we manually set the layout margins. This is exactly what respective plugins do (e.g. @capawesome/capacitor-android-edge-to-edge-support).
For actually using the full screen space on API level 35+, the beforementioned bug in the webview causes the safe area inset at the bottom to be missing (the safe area for the system bar). My initial thought was to manually apply the missing bottom safe area for the system bar for API 35+. For some reason, when using ViewCompat.setOnApplyWindowInsets, all insets are set to 0px - even the top inset that is working when not using setOnApplyWindowInsets for API level 35. I don't understand why.
It doesn't matter too much, since I provide all insets manually either way for API level 35+.
private void edgeToEdgeHandler() {
ViewCompat.setOnApplyWindowInsetsListener(bridge.getWebView(), (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
// For devices < API 35, we apply layout margins --> safe-area-insets will be 0
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
mlp.leftMargin = insets.left;
mlp.bottomMargin = insets.bottom;
mlp.rightMargin = insets.right;
mlp.topMargin = insets.top;
v.setLayoutParams(mlp);
} else {
// For devices with API 35 we manually set safe-area inset variables. There is a current issue with the WebView
// (see https://chromium-review.googlesource.com/c/chromium/src/+/6295663/comments/a5fc2d65_86c53b45?tab=comments)
// which causes safe-area-insets to not respect system bars.
// Code based on https://ruoyusun.com/2020/10/21/webview-fullscreen-notch.html
WebView view = this.bridge.getWebView();
float density = getApplicationContext().getResources().getDisplayMetrics().density;
view.evaluateJavascript(String.format("document.documentElement.style.setProperty('--android-safe-area-top', '%spx')", Math.round(insets.top / density)), null);
view.evaluateJavascript(String.format("document.documentElement.style.setProperty('--android-safe-area-left', '%spx')", Math.round(insets.left / density)), null);
view.evaluateJavascript(String.format("document.documentElement.style.setProperty('--android-safe-area-right', '%spx')", Math.round(insets.right / density)), null);
view.evaluateJavascript(String.format("document.documentElement.style.setProperty('--android-safe-area-bottom', '%spx')", Math.round(insets.bottom / density)), null);
}
// Don't pass window insets to children
return WindowInsetsCompat.CONSUMED;
});
}
I call this method in MainActivity.java in the onCreate method.
In your app code use the defined css properties
body {
// Prefer manually defined safe area variables on Android
--ion-safe-area-top: var(--android-safe-area-top, env(safe-area-inset-top));
--ion-safe-area-left: var(--android-safe-area-left, env(safe-area-inset-left));
--ion-safe-area-right: var(--android-safe-area-right, env(safe-area-inset-right));
--ion-safe-area-bottom: var(--android-safe-area-bottom, env(safe-area-inset-bottom));
}
Of course this is a workaround, but it uses edge-to-edge layout on Android 35 (which was the goal), preserves the previous logic on older Android devices and does not break anything for iOS. I would be interested in other opinions and the question on how to support this out of the box in Capacitor 🤔
I went down the rabbit hole a bit further and came up with the following overall solution.
Thank you for sharing your solution!
It seems the keyboard behavior is also broken when targeting Android 35. The height of the webview is not reduced anymore when the software keyboard is visible causing inputs etc. to be potentially hidden behind the keyboard.
I have thus updated the code above to the following to include the software keyboard:
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() | WindowInsetsCompat.Type.ime());
It seems the keyboard behavior is also broken when targeting Android 35. The height of the webview is not reduced anymore when the software keyboard is visible causing inputs etc. to be potentially hidden behind the keyboard.
I have thus updated the code above to the following to include the software keyboard:
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() | WindowInsetsCompat.Type.ime());
I'm not sure if this will help you, but I had the same issue and what solved it for me was this configuration in capacitor.config.ts:
plugins: {
Keyboard: {
resizeOnFullScreen: true
}
}
Docs: https://capacitorjs.com/docs/apis/keyboard#configuration
@gcarpi Thanks! The respective code for resizeOnFullScreen: true also updates the LayoutParams when the keyboard is visible (link). So the outcome is exactly the same 👍🏻
There's a possible issue related to cordova-plugin-inappbrowser, we now observe the app header being pushed and squeezed on top of the screen, just above the content displayed by the plugin. This was observed on API 35 device. So just a brief comment for now, I'll have to get back in detail on the matter next week.
Any update on this one? Just updated to Capacitor 7 and the layouts are not looking good on older Android OS.
Also, what do I need to do with 'adjustMarginsForEdgeToEdge' to get it to function like Capacitor 6. I'm suprised this is not mentioned in the upgrading to Capacitor 7 docs.
@jamie-nzfunds I'm very surprised as well because it makes apps on older Android devices unusable. I would have expected that setting adjustMarginsForEdgeToEdge: 'auto' would make sure that it's looking like before on old devices and new devices would finally support edge-to-edge that can be styled similar to iOS with safe-margins etc.
@jamie-nzfunds & @derWebdesigner My solution for now was to use this plugin: https://github.com/capacitor-community/safe-area
Safe area config:
SafeArea.enable({
config: {
customColorsForSystemBars: true,
statusBarColor: '#00000000', // transparent
statusBarContent: 'dark',
navigationBarColor: '#00000000', // transparent
navigationBarContent: 'dark',
offset: 5
}
})
Keyboard config:
Keyboard: {
resizeOnFullScreen: true
}
CSS:
--ion-safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top));
--ion-safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left));
--ion-safe-area-right: var(--safe-area-inset-right, env(safe-area-inset-right));
--ion-safe-area-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
@gcarpi Thank you very much dear Guilherme. And this is working consistently on newer and older devices with both different navigations (the bottom line and the full bottom menu with the 3 icons) Android is offering?
@gcarpi Thank you very much dear Guilherme. And this is working consistently on newer and older devices with both different navigations (the bottom line and the full bottom menu with the 3 icons) Android is offering?
We’ve tested it from Android 15 down to Android 10, and this plugin fixed the safe area issues for both the top and bottom of the screen.
It’s important to mention that in some places where Ionic doesn’t automatically apply --ion-safe-area-top or --ion-safe-area-bottom — such as when you're not using IonHeader or IonFooter — you’ll need to manually add this correction. In our case, most IonModal components required manual adjustment.
Just for reference: This is my latest version for safe area handling
import android.os.Build
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
@CapacitorPlugin
class SafeAreaPlugin : Plugin() {
private var windowInsets: WindowInsetsCompat? = null
override fun load() {
super.load()
ViewCompat.setOnApplyWindowInsetsListener(bridge.webView) { v: View, windowInsets: WindowInsetsCompat ->
this.windowInsets = windowInsets
applyInsets(windowInsets)
WindowInsetsCompat.CONSUMED
}
}
@PluginMethod
fun initialize(call: PluginCall) {
windowInsets?.let {
applyInsets(it)
}
call.resolve()
}
/**
* When targeting Android API 35, edge-to-edge handling is enforced. We want
* to use edge-to-edge layout on devices > API 35, but have to preserve
* layout margins on older devices - otherwise part of the content is cut
* off (hidden behind the status bar).
* Thus, we use `adjustMarginsForEdgeToEdge: 'disable'` and manually handle
* the case for devices < API 35
*
* open issue: https://github.com/ionic-team/capacitor/issues/7951
*/
private fun applyInsets(windowInsets: WindowInsetsCompat) {
// For devices < API 35, we apply layout margins --> safe-area-insets will be 0
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
val insets =
windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
bridge.activity.runOnUiThread {
bridge.webView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
topMargin = insets.top
rightMargin = insets.right
leftMargin = insets.left
}
}
} else {
val insets =
windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() or WindowInsetsCompat.Type.ime())
// For devices with API 35 we manually set safe-area inset variables. There is a current issue with the WebView
// (see https://chromium-review.googlesource.com/c/chromium/src/+/6295663/comments/a5fc2d65_86c53b45?tab=comments)
// which causes safe-area-insets to not respect system bars.
// Code based on https://ruoyusun.com/2020/10/21/webview-fullscreen-notch.html
val density = bridge.activity.applicationContext.resources.displayMetrics.density
val top = Math.round(insets.top / density)
val right = Math.round(insets.right / density)
val bottom = Math.round(insets.bottom / density)
val left = Math.round(insets.left / density)
bridge.activity.runOnUiThread {
bridge.webView.loadUrl(
"""
javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-top', 'max(env(safe-area-inset-top), ${top}px)');
javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-right', 'max(env(safe-area-inset-right), ${right}px)');
javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-bottom', 'max(env(safe-area-inset-bottom), ${bottom}px)');
javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-left', 'max(env(safe-area-inset-left), ${left}px)');
void(0);
""".trimIndent()
)
}
}
}
}
I use a custom capacitor plugin to support using location.reload() in the app - safe areas are not applied automatically in this case. The initialize() method is then called in my AppComponent.
Using the css properties is the same as written above:
body {
// Prefer manually defined safe area variables on Android
--ion-safe-area-top: var(--android-safe-area-top, env(safe-area-inset-top));
--ion-safe-area-left: var(--android-safe-area-left, env(safe-area-inset-left));
--ion-safe-area-right: var(--android-safe-area-right, env(safe-area-inset-right));
--ion-safe-area-bottom: var(--android-safe-area-bottom, env(safe-area-inset-bottom));
}
My solution for now was to use this plugin: https://github.com/capacitor-community/safe-area
Thanks for the tip, I tried out your suggestion and found there’s a bug in the package: https://github.com/capacitor-community/safe-area/issues/38 It’s been known since October 2024 and still hasn’t been fixed. So I’ve pretty much lost hope they’ll ever address it, which makes the package unusable.
My solution for now was to use this plugin: https://github.com/capacitor-community/safe-area
Thanks for the tip, I tried out your suggestion and found there’s a bug in the package: capacitor-community/safe-area#38 It’s been known since October 2024 and still hasn’t been fixed. So I’ve pretty much lost hope they’ll ever address it, which makes the package unusable.
Have you tried this solution? https://github.com/ionic-team/capacitor/issues/7951#issuecomment-2807407456
Any updates on this? I am still having issues with safe areas not being set (mainly on Android 14)
I use a custom capacitor plugin to support using
location.reload()in the app - safe areas are not applied automatically in this case. Theinitialize()method is then called in myAppComponent.
For anyone that is using this solution: The implementation currently has a bug where the insets are not passed on to the web view if you close and re-open the app. It seems like the web view is refreshed by the system so the CSS variables are lost.
Calling the initialize() method of the plugin in your JS code works around this issue.
The same issue is present in the capacitor-community/safe-area plugin: https://github.com/capacitor-community/safe-area/blob/4a1bd6ed08993fb5099469d293bff3c5f1a8498c/android/src/main/java/com/getcapacitor/community/safearea/SafeArea.kt#L21-L24
I don't know if this will help you, but after spending a few days testing the main solutions available (and not being convinced by any of these), I've discovered that we don't need plugins, adjustMarginsForEdgeToEdge, nor windowOptOutEdgeToEdgeEnforcement to have working edge-to-edge targeting SDK 35 anymore (using css env(safe-area-inset-*)) :
My original main problem (env(safe-area-inset-*) not defined correctly) seems to have been corrected directly in the Android System Webiew since version 136 (released 23.04.2025).
I've seen the issue, and then its resolution by updating manually the webview app on Android emulators 12 (SDK 31) and 15 (SDK 35). (The SDK 36 emulator was already up to date and didn't have the issue).
My personnal and work phones have got the webview version 138.x installed (without manual intervention) and therefore are not impacted anymore.
I tried using the Capacitor plugin for edge-to-edge but the issue wasn’t solved Fully so Finally I fixed it by updating my theme file styles.xml with :
<item name="android:fitsSystemWindows">true</item>
<item name="android:statusBarColor">#FFFFFF</item>
<item name="android:windowLightStatusBar">true</item>
Note : this will only work with Capacitor plugin for edge-to-edge.
Now the layout works fine and it also plays nicely with AdMob banner ads. 🎉
@hoi4 The question is for me why the auto is only applied with SDK > 35. The code at the moment look for windowOptOutEdgeToEdgeEnforcement to see if someone has configured manually in Android to enable margins through "autoMargins'. But why only make this for newer API Level ?
At our side, we do the same exercice and we use only 'force' for our different clients. Safe area seems not correctly calculated and thus ionic use only 0 and overlap with header.
I'll check if I can have 55px as you for Pixel 9.
@TiBz0u My goal was to preserve the previous logic for older Android devices (apply margins to the webview and have safe-areas of 0) while supporting the "new way" on newer devices (no margins to the webview, proper safe-area variables)
i ended up doing this thanks to the code provided by @hoi4 so that i can have edge to edge enable for all devices.
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin
public class SafeAreaPlugin extends Plugin {
private WindowInsetsCompat windowInsets = null;
@Override
public void load() {
super.load();
bridge.getActivity().runOnUiThread(() -> {
// Get the activity's window
Window window = bridge.getActivity().getWindow();
// Set status and navigation bars to transparent
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
});
ViewCompat.setOnApplyWindowInsetsListener(
bridge.getWebView(),
(v, insets) -> {
this.windowInsets = insets;
applyInsets(insets);
return WindowInsetsCompat.CONSUMED;
}
);
}
@PluginMethod
public void initialize(PluginCall call) {
if (windowInsets != null) {
applyInsets(windowInsets);
}
call.resolve();
}
/**
* When targeting Android API 35, edge-to-edge handling is enforced. We want
* to use edge-to-edge layout on devices > API 35, but have to preserve
* layout margins on older devices - otherwise part of the content is cut
* off (hidden behind the status bar).
* Thus, we use `adjustMarginsForEdgeToEdge: 'disable'` and manually handle
* the case for devices < API 35
*
* open issue: https://github.com/ionic-team/capacitor/issues/7951
*/
private void applyInsets(WindowInsetsCompat windowInsets) {
final Insets insets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() | WindowInsetsCompat.Type.ime()
);
// For devices with API 35 we manually set safe-area inset variables. There is a current issue with the WebView
// (see https://chromium-review.googlesource.com/c/chromium/src/+/6295663/comments/a5fc2d65_86c53b45?tab=comments)
// which causes safe-area-insets to not respect system bars.
// Code based on https://ruoyusun.com/2020/10/21/webview-fullscreen-notch.html
final float density = bridge.getActivity().getApplicationContext().getResources().getDisplayMetrics().density;
final long top = Math.round(insets.top / density);
final long right = Math.round(insets.right / density);
final long bottom = Math.round(insets.bottom / density);
final long left = Math.round(insets.left / density);
bridge
.getActivity()
.runOnUiThread(
() -> {
String js =
"javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-top', 'max(env(safe-area-inset-top), " +
top +
"px)');" +
"javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-right', 'max(env(safe-area-inset-right), " +
right +
"px)');" +
"javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-bottom', 'max(env(safe-area-inset-bottom), " +
bottom +
"px)');" +
"javascript:document.querySelector(':root')?.style.setProperty('--android-safe-area-left', 'max(env(safe-area-inset-left), " +
left +
"px)');" +
"void(0);";
bridge.getWebView().loadUrl(js);
}
);
}
}
seems to have been corrected directly in the Android System Webiew since version 136
Agree with this conclusion, updating android system webview in the emulator fixed it and adding the workaround on top of this works sometimes but not always and made things worse. So no need for the workaround I believe.
How is the progress? It would be really nice if this is fixed 👍
While I couldn't get the suggested fix to work, this plugin seems to do the trick for me: @capawesome/capacitor-android-edge-to-edge-support