Right after I bumped targetSdkVersion from 34 to 35 in Google Play Console, I checked one of my apps on a Pixel 7 and the bottom tab bar had slipped behind the gesture navigation pill. The same screen looked perfectly fine in the preview — only the device running Android 15 was breaking. That pattern has been showing up a lot lately.
The cause is that Android 15 now enforces edge-to-edge display for apps targeting SDK 35 or higher. Rork-generated starter code tends to use React Native's SafeAreaView only at the top of the screen, so when Android 15 changes how the system-bar regions are treated, the layout simply stretches all the way down past the gesture navigation area.
I have been shipping personal apps since 2014, and I have been thrown by layout regressions across major OS bumps more times than I would like to admit. Android 15 has been one of the bigger ones — across the apps that make up my roughly 50 million cumulative downloads, a handful needed an outright layout pass when I raised targetSdkVersion. The patterns below are what I settled on after working through this on real projects, and they slot cleanly into Rork-generated code.
First, confirm what you are looking at
Before assuming it is edge-to-edge enforcement, double-check that you are actually running into it.
# Inspect app.json or app.config.ts
"android": {
"targetSdkVersion": 35,
"compileSdkVersion": 35
}If targetSdkVersion is 34 or lower, edge-to-edge is not enforced. Even on Android 15 devices, the system continues to inset your content as before.
That said, Google Play is requiring targetSdkVersion: 35 for apps submitted from August 2026 onward, so if you plan to update the store listing, this is unavoidable. In a Rork-generated Expo project, the value lives in android/app/build.gradle after expo prebuild.
On the device, this one-liner is the fastest way to confirm you are on the right OS:
adb shell getprop ro.build.version.release
# 15
adb shell getprop ro.build.version.sdk
# 35Once you have confirmed "Android 15 device + targetSdkVersion 35 build", move on to the fixes.
Fix 1: Adopt react-native-edge-to-edge
From Expo SDK 53 onward, react-native-edge-to-edge is bundled in as the recommended path. If the Rork output is not using it yet, install it first.
npx expo install react-native-edge-to-edgeThen add this to the android section of app.json:
{
"expo": {
"android": {
"edgeToEdgeEnabled": true
}
}
}This alone will not fix the bottom of the screen. Edge-to-edge is built around the idea that the app handles the insets itself, on the assumption that content will overlap with system UI. You still need to wire up your components.
Fix 2: Pull the bottom inset with useSafeAreaInsets
Screens that rely on SafeAreaView alone do not get a reliable bottom inset under Android 15's new model. Reading the value with useSafeAreaInsets and feeding it into paddingBottom is the dependable path.
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
export default function TabLayout() {
const insets = useSafeAreaInsets();
return (
<Tabs
screenOptions={{
tabBarStyle: {
// Reserve room for Android 15's gesture nav strip
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
height: 56 + (Platform.OS === "android" ? insets.bottom : 0),
},
}}
>
<Tabs.Screen name="index" options={{ title: "Home" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
</Tabs>
);
}Devices using gesture navigation report a small insets.bottom (often around 16dp), while three-button navigation lands closer to 48dp. Do not hardcode either — always read from insets.
Fix 3: Add bottom padding to your scrollable areas
Once the tab bar is fixed, the next bite is ScrollView or FlatList whose last item disappears behind the tab bar. Reserve room with contentContainerStyle.
import { ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function HomeScreen() {
const insets = useSafeAreaInsets();
const TAB_BAR_HEIGHT = 56;
return (
<ScrollView
contentContainerStyle={{
paddingBottom: TAB_BAR_HEIGHT + insets.bottom + 16,
}}
>
{/* list content */}
</ScrollView>
);
}I keep TAB_BAR_HEIGHT as a single constant in something like constants/layout.ts. When you eventually adjust the bar height, every screen stays in sync without hunting for magic numbers.
Fix 4: Stop the header from sliding under the status bar
Bleeding can happen at the top too, especially when Rork sets translucent to true via expo-status-bar.
// app/_layout.tsx
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }} edges={["top"]}>
<StatusBar style="auto" />
<Stack />
</SafeAreaView>
</SafeAreaProvider>
);
}Two things to note: SafeAreaProvider belongs at the very top, and SafeAreaView's edges should be narrowed to ["top"]. If you leave edges unset, iOS pulls the home indicator inset too and the iOS/Android behaviors start to diverge.
Fix 5: Handle bottom sheets and modals
Bottom sheet libraries such as react-native-modal or @gorhom/bottom-sheet also shift under Android 15. Forgetting to add the bottom inset on the sheet itself leaves your primary CTA overlapping the gesture region — a near-guarantee of accidental dismissals.
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export function CommentSheet() {
const insets = useSafeAreaInsets();
return (
<BottomSheet snapPoints={["50%", "90%"]}>
<BottomSheetView style={{ paddingBottom: insets.bottom + 24, flex: 1 }}>
{/* sheet content */}
</BottomSheetView>
</BottomSheet>
);
}Many sheet libraries handle status bar and keyboard internally, so it is worth checking whether their README mentions "edge-to-edge" or "Android 15" support before assuming you have to do it all yourself.
Verification checklist
When you test on real devices, cover at least three configurations:
- A Pixel-class phone with gesture navigation (small
insets.bottom) - A Samsung device pinned to three-button navigation (large
insets.bottom) - An Android 14 or older device (targetSdkVersion 35 alone does not force edge-to-edge there)
Emulators reproduce most of this, but Samsung's One UI can report slightly different insets, so a single physical device pays for itself fast. I keep an inexpensive Galaxy A model around purely for verification.
Prevention — design every screen with insets in mind
If you build new projects with the assumption that edge-to-edge is the default, none of this becomes a problem again. Asking "who owns the insets on this screen?" as you start each layout is a small habit that pays off enormously when the next OS update lands.
My personal rule is to decide up front whether a screen uses SafeAreaView or useSafeAreaInsets, then leave a comment at the top of the file recording the choice. Even on solo projects, that note pays for itself six months later when you revisit the code.
OS updates will keep coming. With a clear inset strategy in place, they stop turning into emergencies. I hope this saves you the half day I spent figuring it out the first time.