Edge-to-Edge UI in Jetpack Compose

Quick answer: Call
enableEdgeToEdge()beforesetContent, then decide which composable owns each inset. Let backgrounds, imagery, and suitable Material bars draw behind system UI; keep important text, list items, buttons, and input controls out of system-bar, cutout, gesture, and IME regions. For a normal Material 3 screen, passScaffold’sinnerPaddingto the screen content exactly once. Do not layersafeDrawingPadding()orimePadding()onto the same content until you know which insets have already been handled.
Edge-to-edge means app content can draw behind the status bar, navigation bar, display cutout, and caption bar. It creates a more continuous surface, but it also removes the automatic safety margin that used to hide many layout mistakes. On Android 15 and later, edge-to-edge is enforced for apps targeting API 35+, so an old bottom button or last list item can become obscured without any visual code change.
Start with the Android 15 migration model
The official Compose edge-to-edge setup guide states that edge-to-edge is enforced on Android 15 (API 35) and later when an app targets API 35 or higher. Call enableEdgeToEdge() so earlier supported Android versions follow the same model too.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AppTheme {
App()
}
}
}
}enableEdgeToEdge() makes the system bars transparent by default, except that three-button navigation can receive a translucent navigation-bar scrim for legibility. The activity API handles system-bar icon appearance automatically. Do not add separate status- and navigation-icon appearance code when using ComponentActivity’s enableEdgeToEdge(); that is a common source of conflicting theme behavior.
Edge-to-edge is not immersive mode. Most product screens should still show the system bars and protect essential UI from them. Hiding system UI is a separate decision for experiences such as media or maps.
Assign every inset one owner
Insets describe areas where system UI or system gestures can cover or conflict with app content. The useful question is not “how do I pad the whole screen?” but “which layer owns this protected space?”
| UI layer | Usually draws behind system UI? | Typical inset owner |
|---|---|---|
| Screen background, decorative artwork, map, video | Yes | No padding, or a deliberate system-bar protection scrim. |
| Material top/bottom app bars and navigation bars | Often | The Material component’s default windowInsets. |
| Scrollable screen content | No for first/last actionable items | Scaffold innerPadding as contentPadding. |
| Custom floating action | Usually not | The scaffold slot, or targeted safeDrawingPadding(). |
| Text input/composer | No while the keyboard is shown | IME-aware content container. |
WindowInsets.safeDrawing covers places where content may be covered, including system bars, display cutouts, and the IME. safeGestures instead protects areas where touch may be confused with system gestures. The WindowInsets reference documents both; choose the smallest inset that matches the actual risk rather than placing blanket padding around every layer.
Use Scaffold as the normal screen boundary
Scaffold is the easiest way to make a Material screen own its chrome and communicate the remaining safe area to its content. Material 3 top app bars, bottom app bars, navigation bars, and sheets apply their own appropriate insets. The content lambda receives the space needed to avoid that chrome and relevant system UI.
@Composable
fun FeedScreen(
onCreatePost: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(title = { Text("Latest posts") })
},
floatingActionButton = {
FloatingActionButton(onClick = onCreatePost) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = "Create post",
)
}
},
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(innerPadding),
contentPadding = innerPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(posts, key = { it.id }) { post ->
PostRow(post)
}
}
}
}For a list, use contentPadding rather than only Modifier.padding(innerPadding): the first and last items can scroll fully clear of the bars while the list itself still occupies the edge-to-edge window. consumeWindowInsets(innerPadding) records that the list has handled the padding, preventing nested children from applying the same space again. The Material 3 insets guide recommends this Scaffold pattern and cautions against adding another broad inset strategy to the same content.
For a static screen, apply .padding(innerPadding) and .consumeWindowInsets(innerPadding) to its root content container. The full walkthrough of these list and static-content variations is in Scaffold and Window Insets in Jetpack Compose.
Keep custom overlays safe without losing the full-bleed background
Some screens need content behind the bars but still have an overlay action that must be tapped reliably. Apply a direct safe-area modifier to that action, not to the background.
@Composable
fun PhotoViewer(onClose: () -> Unit) {
Box(Modifier.fillMaxSize()) {
Image(
painter = painterResource(R.drawable.sample_photo),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
IconButton(
onClick = onClose,
modifier = Modifier
.align(Alignment.TopEnd)
.safeDrawingPadding()
.padding(8.dp),
) {
Icon(
imageVector = Icons.Outlined.Close,
contentDescription = "Close photo",
)
}
}
}The image stays full bleed. Only the close action moves away from a cutout or status bar. safeDrawingPadding() consumes the insets it handles, so a child will not pad for them again. This targeted approach is especially useful for custom headers, maps, media viewers, and floating actions outside a Scaffold.
sample_photo is an illustrative drawable resource. The insets rule is independent of the image source, so use the app’s image loader or another painter as appropriate.
Handle the IME once, at the container that needs it
The on-screen keyboard is an inset too. Set android:windowSoftInputMode="adjustResize" on activities that host text input so the app receives IME insets, then choose one keyboard strategy for the content container.
With a normal Scaffold, its default innerPadding does not include the IME. A scrollable form can add imePadding() after consuming innerPadding:
@Composable
fun ReplyScreen() {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState()),
) {
OutlinedTextField(
value = reply,
onValueChange = { reply = it },
modifier = Modifier.fillMaxWidth(),
label = { Text("Reply") },
)
Button(onClick = onSend) {
Text("Send")
}
}
}
}Do not add imePadding() when the same scaffold already passes IME insets through a custom contentWindowInsets value such as WindowInsets.safeDrawing; that applies bottom space twice. The edge-to-edge guidance also notes a newer ruler-based fitInside(WindowInsetsRulers.Ime.current) option for deeply nested keyboard-aware content. Whichever approach you choose, keep the focused field and submit control reachable as the keyboard animates.
Avoid the system-bar contrast trap
Transparent bars reveal the app surface behind them. Verify that system icons remain legible in both themes and that a three-button navigation bar has intentional protection. enableEdgeToEdge() adapts icons and its default three-button scrim, but some designs need a custom translucent status-bar protection layer—for example, a scrolling image feed with light pixels behind white status icons.
If a screen has a Material bottom bar and the product intentionally wants its color to extend all the way to the three-button navigation area, set window.isNavigationBarContrastEnforced = false on Android 10 (API 29) and later. This removes the platform’s extra translucent protection, so it is only correct after you have verified the bottom bar and navigation icons remain readable in light and dark themes. The edge-to-edge setup guide covers this trade-off.
For custom visual treatment and semantic color pairs, see Color Contrast and Dark Theme Accessibility in Material 3. System-bar legibility is an accessibility concern, not just a decorative choice.
Test the real boundaries, not one emulator screenshot
Edge-to-edge failures often appear only after the system configuration changes. Before release, test each important screen with:
- Gesture navigation and three-button navigation.
- Light and dark themes, including a screen whose background is bright or photographic.
- A display cutout, landscape orientation, split screen, and resizable window where supported.
- The keyboard open, closed, and animating on every text-input flow.
- Long lists: confirm the first and last items, FAB, snackbar, and bottom actions remain visible and tappable.
- Dialogs, sheets, and full-screen media. A full-screen custom
DialogneedsDialogProperties(decorFitsSystemWindows = false)to draw edge-to-edge too.
On Android 15+ with target SDK 35+, make this part of the regular regression suite—not a final migration check. Edge-to-edge affects every activity and every new bottom-aligned control.
Common edge-to-edge mistakes
Padding the entire screen away from the bars
This removes the benefit of edge-to-edge and can leave blank strips around full-bleed content. Let backgrounds and suitable component chrome draw behind bars; inset only content that needs protection.
Applying insets twice
Scaffold innerPadding plus safeDrawingPadding() on the same root commonly produces oversized top and bottom gaps. Pick one inset owner for each layer and use consumeWindowInsets when passing padding down.
Using Modifier.padding() for a lazy list’s inset space
The list may no longer scroll its first and last items naturally through the padded region. Put the values in contentPadding instead.
Treating IME padding as a universal fix
imePadding() is correct only when a parent has not already supplied IME insets. Check the scaffold’s contentWindowInsets and test the keyboard animation before adding it.
Testing only gesture navigation
Three-button navigation, cutouts, and system-bar icon contrast expose different problems. Test the screen under each navigation mode and both themes.
Edge-to-edge checklist
- Call
enableEdgeToEdge()beforesetContentin every activity. - Set
adjustResizefor activities with keyboard input. - Choose an inset owner per layer; do not pad the same region twice.
- Pass
ScaffoldinnerPaddingto static content or a lazy list’scontentPaddingand consume it. - Keep custom overlay controls inside
safeDrawingor the appropriate targeted inset. - Ensure IME-aware content handles keyboard insets exactly once.
- Review system-bar icon contrast and three-button navigation behavior in both themes.
- Test all screen boundaries on devices/configurations that expose them.
Edge-to-edge is successful when the app reaches every pixel without losing any interaction. The system bars become part of the composition, while the content that people must read, tap, and type into remains reliably reachable.