Insets and edge-to-edge
The status bar, the navigation bar, the cutout and the keyboard all reserve space. Insets are the API that tells you how much — and since Android 15, drawing behind them is the default.
A phone screen has parts that belong to the system and parts that belong to your app, and the boundary moves. The status bar's height depends on the cutout; the navigation bar's height depends on the navigation mode; the keyboard appears and disappears. Insets are how the system tells your app where the boundary currently is.
TL;DR#
- Insets are the margins reserved by system UI: status bar, navigation bar, cutout, IME, and system gestures.
WindowInsetsreports them by type — status bars, navigation bars, IME, display cutout, system gestures — and by edge.- Edge-to-edge means the app draws behind those bars and applies the insets itself as padding.
- API 35Android 15 enforces edge-to-edge for apps targeting 35; API 36Android 16 continues it. Gesture navigation arrived in API 29Android 10 and changes both the bottom inset and the side gesture regions.
- The IME is the inset that changes most often, and the one most likely to hide your input field if you ignore it.
Mental model#
Think of a framed noticeboard.
The frame is the system UI. It is always there, and it covers part of the board.
The board is your window. You can choose to keep your notices inside the frame's inner edge — the safe, boring choice — or to let them run under the frame, which looks better but means the frame hides part of them.
Insets are the measurements of the frame: how wide it is on each side, and how much of it is an actual obstruction versus just decoration.
Where the analogy breaks#
A frame is fixed. Insets change constantly — the keyboard slides up, the status bar hides in immersive mode, a foldable changes the cutout geometry — and they arrive as an event stream, not a constant. Your layout must react to each change rather than reading the value once.
The big picture#
Text description of this diagram
A phone frame with toggles for gesture navigation, display cutout, keyboard and edge-to-edge. With gestural navigation the bottom inset is smaller than with three-button navigation; a cutout increases the top inset; the keyboard replaces the bottom inset with its own height. With edge-to-edge on, the app's content extends under the bars and is padded by the inset values; with it off, the window is shrunk to fit between the bars.
Toggle each control and watch the numbers. The important observation is that the app's content is the same in both modes; only the padding and the window bounds change.
Deep dive#
The inset types#
WindowInsets exposes several independent types, each with its own getter:
| Type | What it represents |
|---|---|
| Status bars | the top system bar |
| Navigation bars | the bottom (or side) navigation bar |
| Display cutout | the notch or camera hole |
| IME | the software keyboard |
| System gestures | the region the system reserves for back/home gestures |
| System overlays | other system UI over the app |
Treating them separately matters because they are not interchangeable. Padding by systemBars does not clear the
keyboard; padding by ime does not clear the cutout.
Reading insets in Views#
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
val bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
)
view.updatePadding(
left = bars.left,
top = bars.top,
right = bars.right,
bottom = bars.bottom,
)
insets
}The listener is called whenever the insets change, including when the keyboard opens. Returning the insets unchanged
lets children also receive them; returning WindowInsetsCompat.CONSUMED stops propagation.
Reading insets in Compose#
Scaffold { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
// content
}
}Scaffold applies the system bar and cutout insets to its content padding. For the IME, Modifier.imePadding() is
the usual tool; it animates with the keyboard rather than jumping.
Edge-to-edge#
Edge-to-edge is a mode, not an API: the app's window is laid out to the full display, behind the system bars, and the app is responsible for keeping content out of the obstructed regions.
Why do it? Because the alternative — the system shrinking your window — produces a visible band of background at the edges, and because content that extends under a translucent bar looks better. The cost is that every screen must handle insets correctly, including the keyboard.
The keyboard is an inset, not an event#
Before the IME inset API, apps listened for global layout changes and guessed the keyboard height. That approach was fragile: it could not distinguish the keyboard from other layout changes, and it broke with split-screen and floating keyboards.
The modern approach is to request the IME inset explicitly:
<activity
android:name=".ChatActivity"
android:windowSoftInputMode="adjustResize" />and then read WindowInsetsCompat.Type.ime(). Whether the window resizes or pans is controlled by
windowSoftInputMode; whether the app knows about the keyboard is controlled by the inset listener.
Cutouts and gesture navigation#
A display cutout is a physical obstruction: the camera or sensor area. The cutout inset tells you how far it extends from the edge. The safe approach is to treat the cutout inset as part of the status bar inset for content that must be readable.
Gesture navigation changes the bottom inset: instead of a full navigation bar there is a much smaller gesture region, plus a system gesture inset for the edge-swipe areas. If your app has a horizontally draggable element at the screen edge, the gesture inset tells you where the system's back gesture will compete with yours.
Predictive back and insets#
Predictive back makes the back gesture interactive: the app is drawn during the gesture and can show a preview of where it will go. That means the back gesture is no longer a discrete event the system handles alone — the app participates, which is why it interacts with the gesture inset and with the window's animation.
The inset dispatch order#
Insets do not just arrive at the root view; they are dispatched down the hierarchy, and each view can consume,
modify or pass them on. A view that returns CONSUMED stops the dispatch, which is why a child that pads by the IME
inset can prevent its parent from doing the same and double-padding.
The rule is simple: decide at one level. If a CoordinatorLayout applies the insets, its children should not; if
every child applies its own, the parent should not.
Why the IME inset is different from the others#
Status bar and navigation bar insets change rarely and predictably. The IME inset changes every time the keyboard opens or closes, and often animates during that change.
That is why Compose has Modifier.imePadding() rather than a static padding read: the modifier participates in the
animation, so the content moves with the keyboard rather than jumping after it. In Views, the equivalent is an inset
listener that runs a WindowInsetsAnimationController for a smooth transition.
Immersive mode is not the absence of insets#
An app that hides the status bar still receives the status bar insets when they reappear, and the system tells the app that a transient bar is showing during a swipe. An app that ignores that will have its controls covered for the duration of the swipe.
The practical approach is to apply insets idempotently: padding by the current inset values every time they change, rather than toggling padding on and off. Recomputing from the current state never gets out of sync.
What to do with a cutout in landscape#
In landscape, a top cutout becomes a left or right cutout, and the inset moves with it. Content that was centred under the notch in portrait can be clipped in landscape unless the layout is anchored to the safe area rather than to the screen.
This is the single most common large-screen inset bug, and it is why the inset API is per-edge rather than a single top value.
Testing insets without a device#
adb shell cmd overlay can enable cutout emulation and change the navigation mode, which makes the two hardest
inset conditions testable on a single image. Combine that with window resizing and the keyboard, and most inset bugs
surface in minutes.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 3 | 1.5 Cupcake |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 12 | 3.1 Honeycomb |
|
| 16 | 4.1 Jelly Bean |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 36 | 16 Baklava |
|
Under the hood#
- WindowInsets.javathe inset type model and the per-edge values
- WindowInsetsController.javaappearance and behaviour of the system bars
- InsetsPolicy.javawhich windows contribute insets, and when
- DisplayPolicy.javastatus bar and navigation bar geometry
Curiosity corner#
- The IME inset can be larger than the screen. A floating or split keyboard reports its own bounds, not the full height, which is why a naive "screen height minus visible height" calculation fails.
- Immersive mode does not remove the insets. It hides the bars and reports the change; the app still gets inset callbacks, so it can restore its layout when the bars return.
- A cutout can be on any edge. Most phones put it at the top; some designs put it at the side, which is why the inset API is per-edge rather than top-only.
Try it#
adb shell dumpsys window | grep -i "insets"Example output
mContentInsets=Insets{left=0, top=96, right=0, bottom=132}
mStableInsets=Insets{left=0, top=96, right=0, bottom=132}adb shell cmd overlay enable com.android.internal.display.cutout.emulation.tallExample output
Cutout setadb shell cmd overlay enable com.android.internal.systemui.navbar.gesturalExample output
Navigation mode set to 2Check yourself#
Check yourself
Why does the system deliver input to an app through a socket pair rather than a Binder call?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsBehavior changes: Apps targeting Android 15 or higherdeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 16 or higherdeveloper.android.comaccessed 2026-10-27↩
- docsAdd predictive backdeveloper.android.comaccessed 2026-10-27↩
- docsDisplay content edge-to-edgedeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 11developer.android.comaccessed 2026-10-27↩
- docsLayers and displayssource.android.comaccessed 2026-10-27↩
- docsLarge screens overviewdeveloper.android.comaccessed 2026-10-27↩