Android Edge-to-Edge: A Developer's Guide
android mobile development ui/ux

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

D. Rout

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 combined
  • displayCutout — notch / punch-hole camera cutouts
  • ime — the software keyboard
  • tappableElement — 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 when translucent is 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.

Share

Comments (0)

Join the conversation

Sign in to leave a comment on this post.

No comments yet. to be the first!