Android keyboard + edge-to-edge issue: gray area after keyboard hide and inconsistent resize (Capacitor 8)

Description

Hi everyone,

We are facing a persistent keyboard + layout issue in an Ionic Angular + Capacitor Android app. The behavior appears to be related to keyboard resize and edge-to-edge handling rather than CSS alone.

We’ve tried multiple configurations, but cannot achieve both:

  • correct keyboard push/resize behavior

  • and no gray/blank area after keyboard hide


As you can see a gray space is added above the home tab, this is added by default and stays throughout the application.

Additionally,

when resizeOnFullScreen: true, we face the below issue

WhatsApp Video 2026-04-07 at 15.59.08

when it is false resizeOnFullScreen: false, then only the gray strip resists and this gray strip is not visible while we inspect through chrome inspect

WhatsApp Video 2026-04-07 at 15.58.59

Environment

  • Ionic Angular: ~8.8.x

  • Angular: ~20.3.x

  • Capacitor (core/android/cli): ~8.2.x

  • @capacitorcapacitor/keyboard: ~@capacitor.0.x

  • @capacitor/splash-@capacitorcreen: ~8.0.x

  • @capacitor/status-bar: ~8.0.x

  • Devices tested: Pixel (issue reproducible), also tested on non-Pixel


Current Configuration

Capacitor config:

  • SplashScreen.launchShowDuration = 0

  • SplashScreen.launchAutoHide = false

  • SplashScreen.splashFullScreen = false

  • SplashScreen.splashImmersive = true

  • Keyboard.resize = Ionic

  • Keyboard.resizeOnFullScreen = true

Status bar:

  • overlaysWebView = true (edge-to-edge)

Android:

  • No explicit windowSoftInputMode initially (also tested with adjustResize and adjustNothing)

Current Device : Realme Narzo 30 - android version 12. Also faced issues in pixel devices.


Observed Behavior

We see two conflicting behaviors depending on configuration:

Case 1: resizeOnFullScreen = true

  • Keyboard pushes layout upward correctly

  • BUT a gray area appears at the bottom (resembles incorrect inset handling)

  • Sometimes gray space remains even after keyboard is dismissed

Case 2: resizeOnFullScreen = false

  • Gray area issue disappears

  • BUT keyboard does not push layout upward, input behind the keyboard stays behind until keyboard is closed.

  • Input fields can be hidden → poor UX


Additional Observations

  • Issue is more noticeable on Pixel devices

  • Gray area is visible on device and screen recording

  • Not visible in Chrome DevTools (chrome://inspect), which suggests it may be a native/inset issue rather than DOM/CSS ( only when resizeOnFullScreen = false )

  • Issue becomes more consistent after:

    • navigating between pages

    • opening/closing keyboard multiple times

    • app resume / notification interaction

What We Tried

  • Keyboard resize modes:

    • Ionic

    • Native

    • Body

    • None

  • resizeOnFullScreen:

    • true

    • false

  • windowSoftInputMode:

    • adjustResize

    • adjustNothing

  • Status bar:

    • overlaysWebView true/false
  • Removed safe-area paddings and background issues

  • Ensured prope@null app/theme background (no @null)

Key Problem

There is no configuration that gives both:

  • :white_check_mark: correct keyboard layout shift

  • :white_check_mark: no gray area / leftover inset


Questions

  1. Is this expected behavior with Capacitor 8 + edge-to-edge on Android (especially Android 12 / Pixel)?

  2. Is resizeOnFullScreen known to cause inset inconsistencies in fullscreen mode?

  3. Should keyboard resizing now be handled manually instead of relying on Capacitor resize?

  4. Is there a recommended “stable” configuration for:

    • edge-to-edge enabled

    • proper keyboard push

    • no gray area

Hi, met same issue today (using Angular + Capacitor), the solution using Claude agent is in modifying MainActivity.java :

public class MainActivity extends BridgeActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Capacitor 8's built-in SystemBars plugin sets padding on the WebView's parent,
        // while @capawesome/capacitor-android-edge-to-edge-support sets margins on the WebView
        // itself. Both apply keyboard (IME) insets, causing them to stack and squish the WebView
        // to nearly zero height when the keyboard opens. Override the parent listener to a no-op
        // so only the EdgeToEdge plugin's margins take effect.
        getBridge().getWebView().post(() -> {
            View parent = (View) getBridge().getWebView().getParent();
            ViewCompat.setOnApplyWindowInsetsListener(parent, (v, insets) -> {
                v.setPadding(0, 0, 0, 0);
                return insets;
            });
            getBridge().getWebView().requestApplyInsets();
        });
    }
}

Thank you so much for sharing the fix. I tried the same MainActivity change but the issue still persists on our side.

Would you mind sharing a bit more about your exact working setup so we can compare? It would really help narrow down what’s different between the environments.

Mainly things like:

  • your Capacitor / Ionic / Angular versions

  • Android build setup (compileSdk, targetSdk, minSdk, Gradle/AGP)

  • the Android OS + device model where it works

  • your capacitor.config keyboard settings (resize mode, resizeOnFullScreen, etc.)

  • your full MainActivity and any activity flags in AndroidManifest

  • whether you’re using any edge-to-edge or safe-area related plugins

If you can share those pieces, we can try to replicate the same setup and see where our configuration differs. Thanks again for the help!

Hey,

my final MainActivity.java is following (another edit for other problem had to be made)

package com.my.app

import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.getcapacitor.BridgeActivity;

public class MainActivity extends BridgeActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getBridge().getWebView().post(() -> {
            View webView = getBridge().getWebView();
            View parent = (View) webView.getParent();

            // Neutralize SystemBars plugin's parent insets listener (prevents double-stacking
            // with the EdgeToEdge plugin which handles the WebView directly).
            ViewCompat.setOnApplyWindowInsetsListener(parent, (v, insets) -> {
                v.setPadding(0, 0, 0, 0);
                return insets;
            });

            // Override the EdgeToEdge plugin's WebView listener so it applies only system bar
            // margins and NOT IME (keyboard) margins. Applying IME margins resizes the WebView,
            // which causes Chromium to invalidate the input connection and immediately call
            // hideSoftInputFromWindow — making the keyboard close right after it opens.
            ViewCompat.setOnApplyWindowInsetsListener(webView, (v, insets) -> {
                Insets bars = insets.getInsets(
                    WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
                );
                ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
                mlp.topMargin = bars.top;
                mlp.bottomMargin = bars.bottom;
                mlp.leftMargin = bars.left;
                mlp.rightMargin = bars.right;
                v.setLayoutParams(mlp);
                return WindowInsetsCompat.CONSUMED;
            });

            webView.requestApplyInsets();
        });
    }
}

capacitor.config.ts:

import type {CapacitorConfig} from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'XXX',
  appName: 'XXX',
  webDir: 'XXX/browser',
  plugins: {
    EdgeToEdge:{
      backgroundColor:'#333333'
    }
  }
};

export default config;

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Permissions -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-feature android:name="android.hardware.location.gps" />
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <activity
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
            android:name=".MainActivity"
            android:label="@string/title_activity_main"
            android:theme="@style/AppTheme.NoActionBarLaunch"
            android:launchMode="singleTask"
            android:exported="true"
>
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                
            </intent-filter>
        </activity>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

package.json

{
  "name": "XX",
  "version": "1.10.0",
  "build": "486",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test",
    "sass": "sass --watch ./src/assets/scss:./src/assets/css/",
    "sass-min": "sass ./src/assets/scss:./src/assets/css/ --style compressed"
  },
  "private": true,
  "dependencies": {
    "@angular-slider/ngx-slider": "^17.0.2",
    "@angular/animations": "^17.3.12",
    "@angular/cdk": "^17.3.10",
    "@angular/common": "^17.3.12",
    "@angular/compiler": "^17.3.12",
    "@angular/core": "^17.3.12",
    "@angular/fire": "^17.1.0",
    "@angular/forms": "^17.3.12",
    "@angular/google-maps": "^17.3.10",
    "@angular/material": "^17.3.10",
    "@angular/platform-browser": "^17.3.12",
    "@angular/platform-browser-dynamic": "^17.1.0",
    "@angular/router": "^17.3.12",
    "@capacitor-mlkit/barcode-scanning": "file:../../plugins/capacitor-mlkit/packages/barcode-scanning",
    "@capacitor/android": "^8.2.0",
    "@capacitor/app": "^8.0.1",
    "@capacitor/browser": "^8.0.2",
    "@capacitor/cli": "^8.2.0",
    "@capacitor/clipboard": "^8.0.1",
    "@capacitor/core": "^8.2.0",
    "@capacitor/geolocation": "^8.1.0",
    "@capacitor/google-maps": "^8.0.1",
    "@capacitor/ios": "^8.2.0",
    "@capacitor/status-bar": "^8.0.1",
    "@capawesome/capacitor-android-edge-to-edge-support": "^8.0.0",
    "@googlemaps/markerclusterer": "^2.6.2",
    "@ng-bootstrap/ng-bootstrap": "^16.0.0",
    "@ng-select/ng-select": "^12.0.7",
    "@ngx-translate/core": "^15.0.0",
    "@ngx-translate/http-loader": "^8.0.0",
    "@popperjs/core": "^2.11.8",
    "angularx-flatpickr": "^7.3.0",
    "angularx-qrcode": "^17.0.1",
    "apexcharts": "^3.45.2",
    "barcode-detector": "^3.0.5",
    "bootstrap": "^5.3.2",
    "deepl-node": "^1.21.0",
    "dotenv": "^17.2.3",
    "html2canvas": "^1.4.1",
    "libphonenumber-js": "^1.11.15",
    "ng-apexcharts": "1.10.0",
    "ngx-color-picker": "^16.0.0",
    "ngx-colors": "^3.5.3",
    "ngx-easy-table": "^15.7.0",
    "ngx-editor": "^17.5.4",
    "ngx-image-compress": "^18.1.5",
    "ngx-pull-to-refresh": "^17.0.0",
    "ngx-toastr": "^18.0.0",
    "overlayscrollbars-ngx": "^0.5.1",
    "rxjs": "~7.8.0",
    "sass": "^1.70.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.14.3"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^17.1.2",
    "@angular/cli": "^17.1.2",
    "@angular/compiler-cli": "^17.1.0",
    "@capacitor/assets": "^3.0.5",
    "@types/cleave.js": "^1.4.12",
    "@types/googlemaps": "^3.43.3",
    "@types/jasmine": "~5.1.0",
    "jasmine-core": "~5.1.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.2.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.1.0",
    "ts-mockito": "^2.6.1",
    "typescript": "^5.3.3"
  }
}

gradle-wrapper.prooperties

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

min sdk 25
target sdk 35

Hope it willbe useful.

Had to make more edits due to not scrolling content to visible area once keyboard is open. had to install capacitor/keyboard plugin.

public class MainActivity extends BridgeActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getBridge().getWebView().post(() -> {
            View webView = getBridge().getWebView();
            View parent = (View) webView.getParent();

            // Neutralize SystemBars plugin's parent insets listener (prevents double-stacking
            // with the EdgeToEdge plugin which handles the WebView directly).
            ViewCompat.setOnApplyWindowInsetsListener(parent, (v, insets) -> {
                v.setPadding(0, 0, 0, 0);
                return insets;
            });

            // Override the EdgeToEdge plugin's WebView listener so it applies only system bar
            // margins and NOT IME (keyboard) margins. Applying IME margins resizes the WebView,
            // which causes Chromium to invalidate the input connection and immediately call
            // hideSoftInputFromWindow — making the keyboard close right after it opens.
            ViewCompat.setOnApplyWindowInsetsListener(webView, (v, insets) -> {
                Insets bars = insets.getInsets(
                    WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
                );
                ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
                mlp.topMargin = bars.top;
                mlp.bottomMargin = bars.bottom;
                mlp.leftMargin = bars.left;
                mlp.rightMargin = bars.right;
                v.setLayoutParams(mlp);
                return WindowInsetsCompat.CONSUMED;
            });

            webView.requestApplyInsets();
        });
    }
}

and scrolling to visible area is solved with app.component.ts method on app init

initializeKeyboardScroll() {
    Keyboard.addListener('keyboardDidShow', (info) => {
      document.body.style.paddingBottom = info.keyboardHeight + 'px';
      const el = document.activeElement as HTMLElement;
      if (el && el.tagName !== 'BODY') {
        el.scrollIntoView({behavior: 'smooth', block: 'center'});
      }
    });
    Keyboard.addListener('keyboardDidHide', () => {
      document.body.style.paddingBottom = '';
    });
  }

Hi,
Thanks a lot once again!

We were able to resolve the issue with only updating the capacitor.config file :slight_smile:
here is the file if it helps

import { CapacitorConfig } from '@capacitor/cli';

import { KeyboardResize } from '@capacitor/keyboard';


const config: CapacitorConfig = {

  appId: 'xx',

  appName: 'xx',

  webDir: 'www',

  server: {

    androidScheme: 'https',

  },

  plugins: {

    SplashScreen: {

      launchShowDuration: 0,

      launchAutoHide: false,

      splashFullScreen: false,

      splashImmersive: false,

    },

    StatusBar: {

      overlaysWebView: false,

    },

    CapacitorHttp: {

      enabled: false,

    },

    Keyboard: {

      resize: KeyboardResize.Ionic,

      resizeOnFullScreen: false,

    },

    EdgeToEdge: {

      backgroundColor: '#333333',

    }

  },

  android: {

    allowMixedContent: true,

  },

};




export default config;

and ofcourse like you mentioned - installed "@capawesome/capacitor-android-edge-to-edge-support": "^8.0.0",

1 Like

The @capacitor/status-bar is kinda deprecated. SystemBars is the new plugin.

Also, make sure you are on Capacitor 8.3.0 as there were more safe area improvements there - Release 8.3.0 · ionic-team/capacitor · GitHub.

Hi :waving_hand:

We recently upgraded our build versions and ran into dependency issues. After resolving those, our previous workaround (using StatusBar.overlaysWebView + EdgeToEdge) stopped working on Android.

So we migrated to SystemBars (Capacitor 8.3.0) with this setup:

SplashScreen: {
  launchShowDuration: 0,
  launchAutoHide: false,
  splashFullScreen: false,
  splashImmersive: false,
},
SystemBars: {
  insetsHandling: 'css',
  style: "DARK",
  hidden: false,
  animation: "NONE"
},
Keyboard: {
  resize: KeyboardResize.Ionic,
  resizeOnFullScreen: false,
},

And:

android:windowSoftInputMode="adjustResize"

We also updated CSS to use --safe-area-inset-*.


Current behavior

  • Screen correctly moves above the keyboard :white_check_mark:

  • But a gray appears above the keyboard :cross_mark:

  • The strip is not part of the DOM (not visible in inspect)

  • Happens only when keyboard is opened


What we’ve tried

  • SystemBars + safe-area CSS

  • EdgeToEdge plugin (removed due to conflicts)

  • StatusBar overlay (not reliable on Android 15/16)

  • Various CSS fixes (no effect since issue is native)


Context

  • Capacitor: 8.3.0

  • This seems related to edge-to-edge + WebView + keyboard insets

  • Saw similar issues reported but no clear resolution


Question

Is there any reliable / recommended fix for this gray strip when using SystemBars + adjustResize on Android 15/16?

Or is this currently a known limitation / upstream issue with WebView or Capacitor?

Any guidance would really help :folded_hands: