WebView manages content alignment using two viewports: the layout viewport (the page size) and the visual viewport (the part of the page the user actually sees). While the layout viewport is generally static, the visual viewport changes dynamically when users zoom, scroll, or when system UI elements (such as the software keyboard) appear.
Feature compatibility
WebView support for window insets has evolved over time to align web content behavior with native Android app expectations:
| Milestone | Feature added | Scope |
|---|---|---|
| M136 | displayCutout() and systemBars() support through CSS safe-area-insets. |
Fullscreen WebViews only. |
| M139 | ime() (input method editor, which is a keyboard) support through visual viewport resizing. |
All WebViews. |
| M144 | displayCutout() and systemBars() support. |
All WebViews (regardless of fullscreen state). |
For more information, see WindowInsetsCompat.
Core mechanics
WebView handles insets through two primary mechanisms:
Safe areas (
displayCutout,systemBars): WebView forwards these dimensions to web content through CSS safe-area-inset-* variables. This enables developers to prevent their own interactive elements (like navigation bars) from being obscured by notches or status bars.Visual viewport resizing using the input method editor (IME): Starting in M139, the input method editor (IME) directly resizes the visual viewport. This resizing mechanism is also based on the WebView-Window intersection. For example, in Android multitasking mode, if the bottom of a WebView extends 200dp below the bottom of the window, the visual viewport is 200dp smaller than the size of the WebView. This visual viewport resizing (for both IME and WebView-Window intersection) is only applied to the bottom of the WebView. This mechanism doesn't support resizing for left, right, or top overlap. This means that docked keyboards appearing on those edges don't trigger a visual viewport resize.
Previously, the visual viewport remained fixed, often hiding input fields behind the keyboard. By resizing the viewport, the visible portion of the page becomes scrollable by default, ensuring users can reach obscured content.
Bounds and overlap logic
WebView should receive non-zero inset values only when system UI elements (bars, display cutouts, or the keyboard) directly overlap with the WebView's screen bounds. If a WebView doesn't overlap with these UI elements (such as, if a WebView is centered on the screen and doesn't touch the system bars), it should receive those insets as zero.
To override this default logic and provide the web content with the complete
system dimensions regardless of overlap, use the
setOnApplyWindowInsetsListener method and return the original, unmodified
windowInsets object from the listener. Providing complete system dimensions
can help ensure design consistency by enabling web content to align with the
device hardware regardless of the WebView's current position. This ensures a
smooth transition as the WebView moves or expands to touch the screen edges.
Kotlin
ViewCompat.setOnApplyWindowInsetsListener(myWebView) { _, windowInsets ->
// By returning the original windowInsets object, we override the default
// behavior that zeroes out system insets (like system bars or display
// cutouts) when they don't directly overlap the WebView's screen bounds.
windowInsets
}
Java
ViewCompat.setOnApplyWindowInsetsListener(myWebView, (v, windowInsets) -> {
// By returning the original windowInsets object, we override the default
// behavior that zeroes out system insets (like system bars or display
// cutouts) when they don't directly overlap the WebView's screen bounds.
return windowInsets;
});
Manage resize events
Because keyboard visibility now triggers a visual viewport resize, web code might see more frequent resize events. Developers must ensure their code doesn't react to these resize events by clearing element focus. Doing so creates a loop of focus loss and keyboard dismissal that prevents user input:
- The user focuses on an input element.
- The keyboard appears, triggering a resize event.
- The website's code clears focus in response to the resize.
- The keyboard hides because the focus was lost.
To mitigate this behavior, review web-side listeners to ensure that viewport
changes don't unintentionally trigger the blur() JavaScript function or
focus-clearing behaviors.
Implement inset handling
WebView's default settings work automatically for most apps. However, if your
app uses custom layouts (for example, if you add your own padding to account for
the status bar or keyboard), you can use the following approaches to improve how
the web content and native UI work together. If your native UI applies padding
to a container based on WindowInsets, you must manage these insets
correctly before they reach the WebView to avoid double-padding.
Double-padding is a situation where the native layout and the web content apply the same inset dimensions, resulting in redundant spacing. For example, imagine a phone with a 40px status bar. Both the native view and the WebView see the 40px inset. Both add 40px of padding, resulting in the user seeing an 80px gap at the top.
The Zeroing approach
To prevent double-padding, you must ensure that after a native view uses an
inset dimension for padding, you reset that dimension to zero using
Insets.NONE on a new WindowInsets object before passing the modified object
down the view hierarchy to the WebView.
When applying padding to a parent view, you should generally use the zeroing
approach by setting Insets.NONE instead of WindowInsetsCompat.CONSUMED.
Returning WindowInsetsCompat.CONSUMED might work in certain situations.
However, it can run into issues if your app's handler changes insets or adds its
own padding. The zeroing approach doesn't have these limitations.
Avoid ghost padding by zeroing insets
If you consume the insets when the app has previously passed non-consumed insets, or if the insets change (like the keyboard hiding), consuming them prevents the WebView from receiving the necessary update notification. This can cause the WebView to retain ghost padding from a previous state (for example, keeping keyboard padding after the keyboard is hidden).
The following example shows a broken interaction between the app and WebView:
- Initial state: The app initially passes non-consumed insets (for example,
displayCutout()orsystemBars()) to the WebView, which internally applies padding to the web content. - State change and error: If the app changes state (for example, the
keyboard hides) and the app chooses to handle the resulting insets by
returning
WindowInsetsCompat.CONSUMED. - Notification blocked: Consuming the insets prevents the Android system from sending the necessary update notification down the view hierarchy to the WebView.
- Ghost padding: Because the WebView doesn't receive the update, it retains the padding from the previous state, causing ghost padding (for example, keeping keyboard padding after the keyboard is hidden).
Instead, use the WindowInsetsCompat.Builder to set the handled types to
zero before passing the object to the child views. This informs the WebView that
those specific insets have already been accounted for while enabling the
notification to continue down the view hierarchy.
Kotlin
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, windowInsets ->
// 1. Identify the inset types you want to handle natively
val types = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
// 2. Extract the dimensions and apply them as padding to the native container
val insets = windowInsets.getInsets(types)
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
// 3. Return a new WindowInsets object with the handled types set to NONE (zeroed).
// This informs the WebView that these areas are already padded, preventing
// double-padding while still allowing the WebView to update its internal state.
WindowInsetsCompat.Builder(windowInsets)
.setInsets(types, Insets.NONE)
.build()
}
Java
ViewCompat.setOnApplyWindowInsetsListener(rootView, (view, windowInsets) -> {
// 1. Identify the inset types you want to handle natively
int types = WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout();
// 2. Extract the dimensions and apply them as padding to the native container
Insets insets = windowInsets.getInsets(types);
rootView.setPadding(insets.left, insets.top, insets.right, insets.bottom);
// 3. Return a new Insets object with the handled types set to NONE (zeroed).
// This informs the WebView that these areas are already padded, preventing
// double-padding while still allowing the WebView to update its internal
// state.
return new WindowInsetsCompat.Builder(windowInsets)
.setInsets(types, Insets.NONE)
.build();
});
How to opt out
To disable these modern behaviors and return to legacy viewport handling, do the following:
Intercept insets: Use
setOnApplyWindowInsetsListeneror overrideonApplyWindowInsetsin aWebViewsubclass.Clear insets: Return a consumed set of insets (for example,
WindowInsetsCompat.CONSUMED) from the start. This action prevents the inset notification from propagating to the WebView altogether, effectively disabling modern viewport resizing and forcing the WebView to retain its initial visual viewport size.