Summary
On Android, on the native-padding branch (shouldPassthroughInsets == false), the soft keyboard is subtracted twice from the web viewport. setupSafeAreaInsets() correctly removes the input-method-editor (IME) inset as decor-view padding, but then returns WindowInsetsCompat with only systemBars() | displayCutout() zeroed — Type.ime() is left live. The System WebView installs its own inset listener (Chromium ≥ 140 handles safe-area and IME insets natively, which is what WEBVIEW_VERSION_WITH_SAFE_AREA_CORE_FIX exists for) and removes the same IME inset a second time.
This is the sibling of #107, on the other branch and by a different mechanism: #107 is about an ungated v.setPadding(...), this is about what the listener dispatches onward. #107 was closed as stale, so this branch's version has never been looked at.
Affected code
android/src/main/java/com/getcapacitor/community/safearea/SafeAreaPlugin.java, in setupSafeAreaInsets(), the shouldPassthroughInsets == false path (~line 137 in 8.0.1):
// We need to correct for a possible shown IME
v.setPadding(systemBarsInsets.left, systemBarsInsets.top, systemBarsInsets.right,
keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom); // <-- applied once, correctly
// Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
// So we have to explicitly set insets to `0`
// See: https://issues.chromium.org/issues/461332423
return new WindowInsetsCompat.Builder(windowInsets).setInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), // <-- ime() missing
Insets.of(0, 0, 0, 0)
).build();
The comment above that setInsets already states the rule the fix follows: zero what you have applied, so nothing downstream re-applies it. Type.ime() was applied by the setPadding immediately above and is not in the mask.
Reproduction / measurement
Samsung SM-S931B (Galaxy S25), Android 16, 1080×2340, density 480, System WebView 150.0.7871.181, Capacitor 8.5.0, targetSdkVersion 36, EdgeToEdge.enable(), SystemBars.insetsHandling: "disable", and SafeArea config { initialViewportFitCover: false, detectViewportFitCoverChanges: false } — i.e. the native-padding branch forced on, deliberately, because most of the app's screens never consumed env(safe-area-inset-*) in CSS.
Keyboard closed: window.innerHeight 697, visualViewport.height 697.7.
Keyboard open, unmodified 8.0.1: innerHeight 61, visualViewport.height 61.7.
Keyboard open, with the fix below: innerHeight 403, visualViewport.height 403.7 — i.e. 1211 / 3.0, the single correct shrink.
An instrumented listener walking the real view chain, keyboard open, before the fix:
WEBVIEW-RECEIVES bars=Insets{...bottom=0} ime=Insets{...bottom=1026} imeVisible=true
DecorView h=2340 padT=103 padB=1026 fitsSW=false
LinearLayout h=1211 padB=0 fitsSW=true
FrameLayout h=1211 padB=0
FitWindowsLinearLayout id=action_bar_root h=1211 padB=0 fitsSW=true
ContentFrameLayout id=content h=1211 padB=0
CoordinatorLayout h=1211 padB=0
CapacitorWebView id=webview h=1211 padB=0
Two notes for anyone reproducing this, both of which cost time here:
uiautomator dump cannot answer this. It reports the android.webkit.WebView node at 185px, which is Chromium's own accessibility root reporting its shrunken viewport, not the View box. The View really is 1211px. The collapse is inside the WebView.
- No native ancestor is involved.
action_bar_root carries fitsSystemWindows="true" — the obvious suspect — and applies zero padding, precisely because the plugin already zeroed the inset types legacy fitSystemWindows consumes. Leaving ime() live is what reaches the WebView.
Invariant across interactive-widget resizes-content / overlays-content / absent, and independent of android:windowSoftInputMode, for the same reason #107 gives: the second subtraction lives in this native inset listener, not in any of those knobs.
Proposed fix
return new WindowInsetsCompat.Builder(windowInsets).setInsets(
- WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
+ WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() | WindowInsetsCompat.Type.ime(),
Insets.of(0, 0, 0, 0)
).build();
Verified on device via patch-package. Two independent interventions reach the same corrected value, which is what makes the attribution solid: clobbering the WebView's own inset listener from the host activity also gives innerHeight 403, but only by replacing Chromium's listener wholesale — correcting the dispatch is the narrow fix and leaves the WebView's own inset handling intact.
Note the passthrough branch has the same defect by the same mechanism (it also leaves ime() live in what it returns, on top of #107's ungated setPadding). I have not proposed a diff for it because this app never takes that branch and I could not measure it, but it looks like the same one-token change plus whatever #107 concluded.
Happy to open a pull request with just the diff above if that is easier to take than a report.
Summary
On Android, on the native-padding branch (
shouldPassthroughInsets == false), the soft keyboard is subtracted twice from the web viewport.setupSafeAreaInsets()correctly removes the input-method-editor (IME) inset as decor-view padding, but then returnsWindowInsetsCompatwith onlysystemBars() | displayCutout()zeroed —Type.ime()is left live. The System WebView installs its own inset listener (Chromium ≥ 140 handles safe-area and IME insets natively, which is whatWEBVIEW_VERSION_WITH_SAFE_AREA_CORE_FIXexists for) and removes the same IME inset a second time.This is the sibling of #107, on the other branch and by a different mechanism: #107 is about an ungated
v.setPadding(...), this is about what the listener dispatches onward. #107 was closed as stale, so this branch's version has never been looked at.Affected code
android/src/main/java/com/getcapacitor/community/safearea/SafeAreaPlugin.java, insetupSafeAreaInsets(), theshouldPassthroughInsets == falsepath (~line 137 in 8.0.1):The comment above that
setInsetsalready states the rule the fix follows: zero what you have applied, so nothing downstream re-applies it.Type.ime()was applied by thesetPaddingimmediately above and is not in the mask.Reproduction / measurement
Samsung SM-S931B (Galaxy S25), Android 16, 1080×2340, density 480, System WebView 150.0.7871.181, Capacitor 8.5.0,
targetSdkVersion 36,EdgeToEdge.enable(),SystemBars.insetsHandling: "disable", andSafeAreaconfig{ initialViewportFitCover: false, detectViewportFitCoverChanges: false }— i.e. the native-padding branch forced on, deliberately, because most of the app's screens never consumedenv(safe-area-inset-*)in CSS.Keyboard closed:
window.innerHeight697,visualViewport.height697.7.Keyboard open, unmodified 8.0.1:
innerHeight61,visualViewport.height61.7.Keyboard open, with the fix below:
innerHeight403,visualViewport.height403.7 — i.e.1211 / 3.0, the single correct shrink.An instrumented listener walking the real view chain, keyboard open, before the fix:
Two notes for anyone reproducing this, both of which cost time here:
uiautomator dumpcannot answer this. It reports theandroid.webkit.WebViewnode at 185px, which is Chromium's own accessibility root reporting its shrunken viewport, not the View box. The View really is 1211px. The collapse is inside the WebView.action_bar_rootcarriesfitsSystemWindows="true"— the obvious suspect — and applies zero padding, precisely because the plugin already zeroed the inset types legacyfitSystemWindowsconsumes. Leavingime()live is what reaches the WebView.Invariant across
interactive-widgetresizes-content/overlays-content/ absent, and independent ofandroid:windowSoftInputMode, for the same reason #107 gives: the second subtraction lives in this native inset listener, not in any of those knobs.Proposed fix
Verified on device via
patch-package. Two independent interventions reach the same corrected value, which is what makes the attribution solid: clobbering the WebView's own inset listener from the host activity also givesinnerHeight403, but only by replacing Chromium's listener wholesale — correcting the dispatch is the narrow fix and leaves the WebView's own inset handling intact.Note the passthrough branch has the same defect by the same mechanism (it also leaves
ime()live in what it returns, on top of #107's ungatedsetPadding). I have not proposed a diff for it because this app never takes that branch and I could not measure it, but it looks like the same one-token change plus whatever #107 concluded.Happy to open a pull request with just the diff above if that is easier to take than a report.