React Native Edge-to-Edge on Android 15 with Expo: SafeAreaView, StatusBar, and Insets in 2026

Fix React Native edge-to-edge on Android 15 with Expo. Learn SafeAreaProvider, useSafeAreaInsets, expo-status-bar, and expo-navigation-bar patterns.

Android 15 Edge-to-Edge in Expo (2026)

Updated: July 3, 2026

Edge-to-edge on Android 15 means your React Native app draws content behind the status and navigation bars by default, and once you're on targetSdkVersion 35, you can't opt out. If you upgrade to Expo SDK 52 or later, the new architecture flips edgeToEdgeEnabled on for you. That's exactly why your carefully centred header suddenly slides under the status bar, and your bottom tab bar hides beneath the gesture pill. Fixing it takes three moving parts: react-native-safe-area-context insets, expo-status-bar, and expo-navigation-bar. This guide walks through each one.

  • Google made edge-to-edge mandatory for apps targeting Android SDK 35 (Android 15) and above. Play Store submissions after August 31, 2025 must use it.
  • Expo SDK 52+ enables edge-to-edge automatically via the android.edgeToEdgeEnabled config, and SDK 55 makes it non-optional in the New Architecture.
  • Wrap your app in SafeAreaProvider and use useSafeAreaInsets() instead of the old SafeAreaView. The hook works on both iOS and Android and respects the gesture navigation pill.
  • Set <StatusBar style="auto" translucent /> from expo-status-bar. Never set an opaque backgroundColor on Android 15+ (it's ignored).
  • For the navigation bar, use expo-navigation-bar to control button colour and visibility, but call it inside an effect, not at module scope.
  • Test on a device or emulator running 3-button navigation, not just the gesture bar. That's where 90% of the bugs surface.

What is edge-to-edge on Android?

Edge-to-edge is Google's term for letting an app draw underneath the system bars (the status bar at the top and the navigation bar or gesture pill at the bottom) instead of leaving reserved padding for them. On the web, this is closer to how a fullscreen page behaves; the browser chrome floats on top of your content rather than pushing it down. If you came to React Native from React on the web, honestly the mental model that helps me most is this: the status bar on Android 15+ is a position: fixed overlay, and your layout is the document beneath it. Nothing pushes your header down anymore. You have to add the padding yourself using insets.

Under the hood, Android calls WindowCompat.setDecorFitsSystemWindows(window, false), which is what Expo's edgeToEdgeEnabled flag ultimately triggers via the react-native-edge-to-edge library that ships with SDK 52. The result: system bars become transparent (or a translucent scrim), and your React Native root view fills the whole window from top to bottom. Any component that assumes it starts at y = 0 with no obstruction (a hero image, a sticky header, a floating action button) will render behind the bars until you consume the correct insets.

Why edge-to-edge is now mandatory for Android 15+

Google's Android developer docs on edge-to-edge made this explicit in the Android 15 platform release: apps targeting targetSdkVersion 35 or higher automatically enter edge-to-edge, and the previous opt-out flags (fitsSystemWindows, android:windowTranslucentStatus, and the deprecated setStatusBarColor) are silently ignored. As of the November 1, 2024 Play policy update, new apps and updates submitted after August 31, 2025 must target SDK 35, which is why so many teams are hitting this at the same time.

Expo tracks this in lockstep. SDK 52 introduced android.edgeToEdgeEnabled: true as the default for new projects, backed by the same react-native-edge-to-edge package. SDK 53 tightened the New Architecture story and shipped React Native 0.79 with edge-to-edge as the assumed baseline. By SDK 55 (April 2026), setting the flag to false is essentially unsupported. It still parses, but Google Play won't accept a build that circumvents edge-to-edge for a targetSdk 35+ app. If you're planning the migration, the React Native New Architecture migration checklist and the Expo SDK 55 migration guide both cover the surrounding upgrade work you'll want to sequence with this.

The upside is that edge-to-edge is genuinely nicer to look at once your layout respects it. Content flows to the physical edges of the screen, dark and light UIs feel more integrated, and gesture navigation stops eating into your tab bars. The downside is that every screen in your app needs to be re-audited for insets. Don't skip that audit; a single hardcoded paddingTop: 20 can break the whole feel. (I learned that one shipping a v2 of a fitness app last year. Two hours of "why is our header off by a hair" turned out to be exactly this.)

Enabling edge-to-edge in an Expo project

In a fresh Expo SDK 52+ project, edge-to-edge is already on. To confirm, or to enable it in a project that was created before SDK 52, edit app.json (or app.config.ts) and set the Android edgeToEdgeEnabled flag:

{
  "expo": {
    "name": "MyApp",
    "android": {
      "package": "com.example.myapp",
      "edgeToEdgeEnabled": true,
      "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png" }
    }
  }
}

After you save that, run npx expo prebuild --clean so the native android/ directory picks up the change, then re-run npx expo run:android. If you're on the managed workflow with EAS Build, the flag propagates automatically on the next build (no prebuild needed). Under the hood, Expo installs and configures react-native-edge-to-edge, which patches the Android theme to Theme.EdgeToEdge and disables the legacy decor-fitting behaviour.

You also need react-native-safe-area-context (it ships with Expo already) and, optionally, expo-navigation-bar if you plan to tint the navigation bar. Verify the versions in your package.json:

npx expo install react-native-safe-area-context expo-status-bar expo-navigation-bar

Using react-native-safe-area-context correctly

The single most useful primitive for edge-to-edge is the useSafeAreaInsets() hook from react-native-safe-area-context. It returns the current top, bottom, left, and right insets in DIPs, updated when the device rotates or when the user switches between gesture navigation and 3-button navigation. Wrap your app root in SafeAreaProvider once, then read insets from any component in the tree.

// App.tsx
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
import RootNavigator from './src/navigation/RootNavigator';

export default function App() {
  return (
    <SafeAreaProvider>
      <RootNavigator />
      <StatusBar style="auto" />
    </SafeAreaProvider>
  );
}

SafeAreaView vs SafeAreaProvider vs useSafeAreaInsets

Three things share the "safe area" name and it trips up almost everyone. Here's how I keep them straight:

  • SafeAreaProvider: the context provider. Mount it exactly once at the root of your app so the hooks below have data to read.
  • useSafeAreaInsets(): a hook returning { top, bottom, left, right }. This is what you'll use in 90% of components. It gives you the numbers, so you can add them wherever you need padding (a header, a modal, a floating button).
  • SafeAreaView: a component that automatically applies the insets as padding. Fine for simple screens, but it forces you into a fixed edge combination and can't share the padding between two adjacent components. For anything non-trivial, prefer the hook.

A typical screen looks like this:

// screens/HomeScreen.tsx
import { View, Text, ScrollView } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function HomeScreen() {
  const insets = useSafeAreaInsets();

  return (
    <View style={{ flex: 1, backgroundColor: '#fff' }}>
      {/* Header sits under the status bar; add top inset as padding */}
      <View style={{ paddingTop: insets.top, backgroundColor: '#0f172a' }}>
        <Text style={{ color: 'white', padding: 16, fontSize: 20 }}>Home</Text>
      </View>

      <ScrollView
        contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}
      >
        {/* content */}
      </ScrollView>
    </View>
  );
}

Notice that the outer View has no inset; it fills the whole screen. The insets are applied to specific children: paddingTop on the header, paddingBottom on the scroll content. This is the pattern that scales. Whenever a designer asks for "colour behind the status bar," you already have it, because the background extends all the way up.

Handling the status bar with expo-status-bar

The expo-status-bar component controls the status bar's foreground (icon colour) but not the background. Under edge-to-edge, the background is always transparent by design. Set the style prop to "light", "dark", or "auto" to match the underlying view. The translucent prop is now the default and mostly redundant, but I still pass it for clarity.

// Inside SafeAreaProvider
import { StatusBar } from 'expo-status-bar';

<StatusBar style="light" translucent />

// Or make it react to your theme (see the dark mode guide)
import { useColorScheme } from 'react-native';
const scheme = useColorScheme();
<StatusBar style={scheme === 'dark' ? 'light' : 'dark'} />

If different screens need different icon colours, drop a <StatusBar /> inside each screen. Expo's implementation is push-based and the most recently mounted one wins. This is the trick I use for a dark-themed home screen followed by a white settings screen: no global state needed, each screen owns its own bar. For a deeper dive on syncing this with your theme, the React Native dark mode guide covers useColorScheme, NativeWind v4 and the status bar together.

The navigation bar (the row containing the back, home, and recents buttons, or the gesture pill on newer devices) is the second half of the edge-to-edge puzzle. Under Android 15, it's transparent by default, and any button icons are drawn in a colour that Android calculates from the underlying content. That works for content-heavy screens, but on a plain white background with black icons over a semi-transparent bar, the bar can appear muddy. expo-navigation-bar gives you programmatic control:

// components/ThemeSync.tsx
import { useEffect } from 'react';
import { Platform } from 'react-native';
import * as NavigationBar from 'expo-navigation-bar';

export function ThemeSync({ isDark }: { isDark: boolean }) {
  useEffect(() => {
    if (Platform.OS !== 'android') return;

    // Icon colour: 'light' means light icons on a dark surface
    NavigationBar.setButtonStyleAsync(isDark ? 'light' : 'dark');

    // Behaviour when the user swipes from the bottom
    NavigationBar.setBehaviorAsync('overlay-swipe');
  }, [isDark]);

  return null;
}

Two important caveats. First, do not call setBackgroundColorAsync on Android 15 targeting SDK 35. The API is deprecated and silently ignored. Google now requires the bar to be transparent under edge-to-edge, and if you want a "coloured" bar, you achieve it by drawing your own bottom container with the inset padding applied. Second, always guard on Platform.OS === 'android'. The module is a no-op on iOS but the call still incurs a bridge hop.

For a bottom tab bar that should visually merge with the navigation bar, the pattern is: use the bottom inset as padding on your tab container and let the container's background colour show through the transparent navigation bar. Combined with useSafeAreaInsets(), this replaces every ad-hoc marginBottom: 20 you may have littered through the codebase.

Common edge-to-edge bugs and how to fix them

These are the five bugs I've seen most often when migrating apps to edge-to-edge over the last few months. If you're staring at a broken screen, work through them in order:

  1. Header content clipped under the status bar. You forgot to add paddingTop: insets.top to the header container, or you added it to a parent that isn't the actual visual header. Move the padding onto the coloured View, not the outer SafeAreaView.
  2. Bottom tab bar hidden behind the gesture pill. Your tab navigator's inner container needs paddingBottom: insets.bottom. React Navigation's bottom tabs handle this if you pass tabBarStyle without overriding height; hard-coding a fixed height breaks it.
  3. Modal or bottom sheet content clipping. When you present a modal with presentationStyle="pageSheet" or a Gorhom bottom sheet, add insets.bottom to the modal's scroll padding. See the Gorhom bottom sheet tutorial for the full pattern.
  4. Keyboard covers the input field. KeyboardAvoidingView with behavior="padding" plus a keyboardVerticalOffset equal to the top inset usually fixes it, but the keyboard handling comparison outlines why react-native-keyboard-controller is a better answer for anything complex.
  5. Insets are all zero. You mounted SafeAreaProvider below the component reading insets, or you're rendering outside of it (a portal, a modal without the provider re-wrapped). Move SafeAreaProvider to the very root, above NavigationContainer.

Testing edge-to-edge across devices

Don't trust the emulator alone. So, Android's edge-to-edge behaviour changes based on three inputs that the emulator handles inconsistently: gesture navigation vs 3-button navigation, screen shape (notch, hole punch, foldable inner display), and the current dark mode. Here's the minimum test matrix I run before shipping an edge-to-edge migration:

  • Pixel 8 or 9 emulator, gesture navigation, light mode. This is the happy path; most bugs won't show here.
  • Same emulator switched to 3-button navigation (Settings, System, Gestures). The bottom inset roughly doubles, so anything that assumed a small bottom pad will look wrong.
  • A physical mid-range device, ideally Samsung. Samsung's One UI applies its own navigation bar behaviour on top of stock Android, and it's where I've caught the most surprises, especially with the split status bar and the notification shade.
  • Foldable inner display, if you support tablets. The status bar height changes when the device unfolds. If you cached inset values in a variable instead of using the hook, this is where it breaks.
  • Landscape rotation. Insets update. Verify your top bar doesn't animate awkwardly during the rotation.

Automate what you can with Maestro: flow through the app in gesture and 3-button modes and screenshot each screen. The full setup is in the testing React Native apps guide. For manual QA, I keep a checklist screen inside my dev builds that renders all four insets numerically and colours the safe areas. Being able to see the numbers on the actual device shortcuts most debugging sessions.

Frequently Asked Questions

Do I need to enable edge-to-edge in Expo SDK 52 and later?

New Expo SDK 52+ projects have edgeToEdgeEnabled: true set by default in the generated app.json. If you upgraded from an older SDK, add the flag manually under expo.android and run npx expo prebuild --clean. Once you target Android SDK 35+, you can no longer opt out on the Play Store.

What is the difference between SafeAreaView and useSafeAreaInsets?

SafeAreaView auto-applies inset padding as a component wrapper, which is fine for simple screens. useSafeAreaInsets() returns raw pixel values so you can apply them selectively. For example, you can add top inset to a coloured header while letting the background paint behind the status bar. For most screens under edge-to-edge, the hook is the better choice.

How do I set the status bar background colour on Android 15?

You don't. Android 15 requires the status bar to be transparent under edge-to-edge and ignores StatusBar.setBackgroundColor(). To achieve a "coloured status bar" effect, paint the coloured background on a View behind it and pad the content below with useSafeAreaInsets().top. Use expo-status-bar only to control icon colour.

Why is my status bar overlapping content after upgrading to Expo SDK 55?

Because edge-to-edge is now the default and your layout no longer receives the automatic top padding that older SDKs added. Wrap your app in SafeAreaProvider, then add paddingTop: insets.top to any container that was previously starting at the top of the screen (headers, hero images, sticky banners).

Does edge-to-edge affect iOS in the same way?

iOS has always drawn under the status bar and the home indicator. Edge-to-edge is really just Android catching up to that model. The useSafeAreaInsets() hook works identically on iOS, so the same layout code covers both platforms. The main iOS-specific quirk is Dynamic Island, which is reported as a taller top inset on iPhone 14 Pro and newer.

Anita Iyer
About the Author Anita Iyer

Cross-platform mobile developer who came to RN from web. Bridges the two worlds and explains the seams.