
Android Edge-to-Edge: A Developer's Guide

D. Rout
March 13, 2026 6 min read
On this page
What it is, why Google made it mandatory, and how to handle it across native and hybrid frameworks.
01 — What is Edge-to-Edge? 🖼️
Edge-to-edge is a display mode where your app's content draws behind the system UI — the status bar at the top and the navigation bar at the bottom. Instead of the OS reserving those strips for itself, your app gets the full screen rectangle.
Before edge-to-edge, Android would letterbox your layout: the status bar sat on top, the navigation bar sat on the bottom, and your app lived in the space in between. Edge-to-edge removes that hard boundary — the system bars are drawn on top of your content rather than in front of dedicated reserved strips.
Window Insets 📐
With your layout extending into the system bars, you need a way to know where those bars are so important content isn't hidden. That's what window insets provide: pixel measurements telling your app how much space the system UI occupies on each side.
Key inset types:
systemBars— status bar + navigation bar combineddisplayCutout— notch / punch-hole camera cutoutsime— the software keyboardtappableElement— the minimum area that must be tappable
⚠️ Why Google Made it Mandatory in Android 15
Starting with Android 15 (API level 35), edge-to-edge is enforced for apps targeting that API level or above. If you haven't handled insets, your content may be obscured by the status or navigation bar — the most common breakage from this change.
🚨 Important: If you target API 35+ and haven't added inset handling, your content may be obscured by the status bar or navigation bar. This is the most common breakage introduced by the change.
02 — Native Android (Kotlin / Java) 🤖
Enabling Edge-to-Edge
On API 35+ targeting apps it's on by default. To opt in explicitly, call this in Activity.onCreate before setContentView:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // androidx.activity:activity 1.8.0+
setContentView(R.layout.activity_main)
}
Handling Insets — View System
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
💡 Apply insets to the specific views that need them — top padding on a toolbar, bottom padding on a bottom nav bar — so scrollable content flows freely behind both.
Handling Insets — Jetpack Compose 🧱
Scaffold handles insets automatically:
Scaffold(
topBar = { TopAppBar(title = { Text("My App") }) },
bottomBar = { BottomNavigation { /* ... */ } }
) { innerPadding ->
LazyColumn(contentPadding = innerPadding, modifier = Modifier.fillMaxSize()) {
// content
}
}
For custom layouts, use the windowInsetsPadding modifier:
Box(
modifier = Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.systemBars)
) { /* content */ }
03 — Ionic Capacitor ⚡
Capacitor 6 introduced automatic edge-to-edge support on Android 15. Your WebView extends behind the system bars, and Capacitor exposes inset values as CSS environment variables.
Step 1 — Update MainActivity
Capacitor 6's BridgeActivity calls enableEdgeToEdge() internally. If you're on an older version or upgrading, call it explicitly:
public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(StatusBar.class);
super.onCreate(savedInstanceState);
enableEdgeToEdge(); // add this if on Capacitor < 6
}
}
Step 2 — StatusBar Plugin
import { StatusBar, Style } from '@capacitor/status-bar';
await StatusBar.setOverlaysWebView({ overlay: true });
await StatusBar.setStyle({ style: Style.Dark });
await StatusBar.setBackgroundColor({ color: '#00000000' });
Step 3 — Insets in CSS 🎨
.bottom-bar {
padding-bottom: env(safe-area-inset-bottom);
}
/* Or combine with your own padding */
.bottom-bar {
padding-bottom: calc(env(safe-area-inset-bottom) + 16px);
}
💡 Tip: Ionic components like
<ion-tabs>and<ion-footer>handle safe area insets automatically whentranslucentis set. Prefer these over manual padding when using the full Ionic framework.
capacitor.config.ts
const config: CapacitorConfig = {
plugins: {
StatusBar: {
overlaysWebView: true,
style: 'DARK',
backgroundColor: '#00000000',
},
},
};
04 — Apache Cordova 🔌
Cordova requires a combination of plugins and manual configuration. It has less first-party support than Capacitor, so you'll be doing more of this yourself.
Install Plugins
cordova plugin add cordova-plugin-statusbar
cordova plugin add cordova-plugin-navigationbar-color
config.xml
<platform name="android">
<preference name="StatusBarOverlaysWebView" value="true" />
<preference name="StatusBarBackgroundColor" value="#00000000" />
<preference name="StatusBarStyle" value="lightcontent" />
</platform>
Runtime JS
document.addEventListener('deviceready', () => {
StatusBar.overlaysWebView(true);
StatusBar.styleDefault();
NavigationBar.setUp(true);
}, false);
CSS Insets
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
⚠️ Note: Cordova's Android support is increasingly community-maintained. Capacitor is the recommended migration path for better Android 15 compatibility.
05 — React Native ⚛️
Install react-native-safe-area-context
npm install react-native-safe-area-context
Wrap your app at the root:
import { SafeAreaProvider } from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaProvider>
<YourApp />
</SafeAreaProvider>
);
}
SafeAreaView
import { SafeAreaView } from 'react-native-safe-area-context';
function MyScreen() {
return (
<SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
<YourContent />
</SafeAreaView>
);
}
The edges prop lets you apply safe area padding to only specific sides — useful when a custom tab bar handles its own insets.
useSafeAreaInsets Hook 🪝
For custom layouts, use the hook directly:
const insets = useSafeAreaInsets();
<View style={{ paddingBottom: insets.bottom }}>
<TabItems />
</View>
Enable Edge-to-Edge in MainActivity.kt
import androidx.core.view.WindowCompat
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
🗂️ Quick Reference
| Platform | Auto on API 35? | Inset Method | Key Dependency |
|---|---|---|---|
| Native (Views) | ✅ Yes | setOnApplyWindowInsetsListener |
androidx.activity 1.8+ |
| Jetpack Compose | ✅ Yes | windowInsetsPadding / Scaffold |
compose-foundation 1.5+ |
| Ionic Capacitor | ✅ Cap 6+ | env(safe-area-inset-*) |
@capacitor/status-bar |
| Apache Cordova | ⚠️ Manual | env(safe-area-inset-*) + JS |
cordova-plugin-statusbar |
| React Native | ⚠️ Manual | SafeAreaView / useSafeAreaInsets |
react-native-safe-area-context |
💬 General Advice
Regardless of framework, the mental model is the same: draw everywhere, but pad the things that matter. Scrollable content should flow behind system bars for an immersive feel. But tappable elements — buttons, bottom navigation, input fields — need padding equal to the relevant inset so they're never hidden.
🧪 Test on a real Android 15 device or a Pixel emulator running API 35. Gesture navigation (no visible nav bar) and 3-button navigation have different inset heights — confirm both work correctly.
Read next
Comments (0)
Join the conversation
Sign in to leave a comment on this post.
No comments yet. to be the first!