title: "Handling Safe Areas in React Native Expo: Avoiding Notches and Home Indicators"

date: 2025-04-02

tags:


Handling Safe Areas in React Native Expo: Avoiding Notches and Home Indicators

Have you ever launched your React Native app on a shiny new phone, only to find your content hiding behind a pesky notch or getting cut off by the home indicator? 😱 With modern devices sporting notches, camera cutouts, curved corners, and gesture bars, it's more important than ever to keep your app's UI in the safe area. In this tutorial, we'll dive into what safe areas are and how to handle them in React Native apps built with Expo. We'll cover using the react-native-safe-area-context library – including SafeAreaView, SafeAreaProvider, and the useSafeAreaInsets hook – to ensure your app steers clear of danger zones. Along the way, we'll look at scrollable content, modals, custom headers, and controlling which edges get padded. Let's get started!

What Are Safe Areas and Why Should You Care?

Safe areas refer to the portions of the screen that are free from obstructions by device hardware or OS UI elements. On devices like the iPhone X (and newer), or many Android phones, parts of the screen are occupied by things like the notch (sensor housing), status bar, home gesture indicator, or navigation bar. Content that falls in those regions might be hidden or hard to interact with. Safe areas ensure your content is positioned away from those “danger zones” so nothing important gets overlapped.

Illustration of safe area vs. no safe area on a notched device. On the left, the colored blocks are placed at the screen edges without respecting safe areas – notice how the top blocks would be cut off by the notch and the bottom ones by the home indicator. On the right, after applying safe area insets, the blocks are shifted inward (down from the notch, up from the bottom), ensuring they remain fully visible. Safe areas dynamically adjust these margins so your UI avoids device cutouts and curved corners.

In practical terms, if you don’t account for safe areas, you might see your app’s header tucked under the phone’s status bar or a button hiding behind the home indicator at the bottom. Not only does this look bad, it can make taps or reads impossible. Using safe areas will automatically add the needed padding so that your content stays within visible bounds. The goal is to use as much screen real estate as possible without letting anything important live in the "no-go" zones.

Setting Up the Safe Area Context in Expo

Thankfully, Expo and React Native provide tools to handle safe areas easily. The go-to solution is the react-native-safe-area-context library. Expo actually includes this by default if you used the Expo prebuilt templates or Expo Router (so you might not need to install it separately). If for some reason your project doesn't have it, you can add it via expo install react-native-safe-area-context.

Before we use any safe area features, we should set up a SafeAreaProvider. This component activates the context that calculates the device's safe area insets (the sizes of those top/bottom/left/right bars or cutouts). Typically, you wrap your app's root component with SafeAreaProvider. In an Expo app, open your App.js (or App.tsx) and do something like:

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import HomeScreen from './HomeScreen';

export default function App() {
  return (
    <SafeAreaProvider>
      <HomeScreen />
    </SafeAreaProvider>
  );
}

By wrapping our app in SafeAreaProvider, any child components can now use safe-area-aware views and hooks. (If you're using Expo Router or React Navigation, they might set this up for you under the hood. But it never hurts to wrap it yourself if you're unsure.)

Using SafeAreaView to Avoid Notches and Indicators

The easiest way to keep a screen's content in the safe zone is to use SafeAreaView. This component is a drop-in replacement for a regular <View> that automatically applies padding for the device's safe area boundaries. In other words, it will add extra padding at the top, bottom, left, and right (as needed) so that its children are not obscured by things like the notch or status bar.

Let's say we have a simple home screen component. We can wrap the whole screen in SafeAreaView:

// HomeScreen.js
import { SafeAreaView, StyleSheet, Text } from 'react-native-safe-area-context';
// (Make sure to import SafeAreaView from react-native-safe-area-context, not react-native!)

export default function HomeScreen() {
  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.title}>Hello, safe world!</Text>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    // other styles...
  },
  title: {
    fontSize: 24,
    textAlign: 'center'
  }
});

Here, the SafeAreaView will behave just like a normal View with flex: 1 and our white background, but it will automatically insert padding at the top on an iPhone with a notch, on the bottom on a device with a home bar, etc. So the text "Hello, safe world!" will be nicely below the status bar on iOS and not behind any notification bars on Android.

Under the hood, SafeAreaView uses the context to know the inset sizes and adds that padding (by default it uses padding, but you can configure it to use margin if needed). If you provide your own padding styles, those will be added on top of the safe area padding. This means you can combine your own styling with safe area insets without worrying about them clobbering each other.

Important: React Native does have a built-in SafeAreaView component, but it only works on iOS and has limitations. In a cross-platform Expo app, always import SafeAreaView from react-native-safe-area-context (as we did above) to get it working on both iOS and Android. The safe-area-context version works on iOS 11+ and Android, and even on web, whereas the core one would do nothing on Android.

Using the useSafeAreaInsets Hook for Custom Layouts

Sometimes you need more fine-grained control than a full SafeAreaView wrapper. This is where the useSafeAreaInsets hook comes in handy. This hook gives you direct access to the four inset values: { top, bottom, left, right } in points/pixels. You can then use these values to adjust your styles or layout manually.

For example, imagine you have a custom header component, or a screen where only part of the layout should respect the safe area (while another part might go edge-to-edge). Using the hook, you can get the inset for the top and apply it as padding:

import { Text, View, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export default function CustomHeader() {
  const insets = useSafeAreaInsets();  // get safe area values
  return (
    <View style={[styles.header, { paddingTop: insets.top }]}>
      <Text style={styles.headerText}>My Custom Header</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  header: {
    backgroundColor: '#4c93ff',
    // We will add paddingTop dynamically from insets.top
    paddingHorizontal: 16,
    paddingBottom: 8
  },
  headerText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: '600'
  }
});

In this snippet, we create a header view and manually apply paddingTop: insets.top. So on an iPhone with a notch, insets.top might be, say, 44 points – the view will get 44 pts of padding on top, pushing the header text below the notch. On an older device or Android, insets.top could be 0 (or just the status bar height on Android if applicable), adjusting accordingly. The result is a header that always starts below any system overlap area.

The useSafeAreaInsets hook is great for when you cannot or do not want to wrap an entire area in a SafeAreaView. For instance, if you have an absolutely positioned element, or you only want to apply safe area padding to one edge of a component, the hook lets you grab the numbers and do it yourself. It’s also useful in class components or deeper component trees where wrapping might be inconvenient (there's also a <SafeAreaConsumer> if you need to use render props, but hooks cover most cases).

Note: The hook requires that somewhere above in the component tree, a SafeAreaProvider is present (otherwise insets will just be zeros). In our setup above, we put SafeAreaProvider in App.js, so all screens have context. One caveat: if you use a React Native Modal or other portal, the modal content might not be inside that provider – we’ll talk about a fix for that soon.

Safe Areas in Scrollable Screens

Many apps use ScrollViews or FlatLists that extend beyond the screen. How do we handle safe areas for scrollable content? There are a couple of approaches:

1. Wrap the ScrollView in a SafeAreaView: This is the simplest method. For example:

import { SafeAreaView } from 'react-native-safe-area-context';
import { ScrollView, Text } from 'react-native';

export default function ProfileScreen() {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ScrollView contentContainerStyle={{ padding: 16 }}>
        <Text style={{ fontSize: 18 }}>...Your scrollable content...</Text>
        {/* ...more content... */}
      </ScrollView>
    </SafeAreaView>
  );
}

Here, the SafeAreaView ensures the ScrollView (and all its content) starts below the top safe area and ends above the bottom safe area. So even if the user scrolls to the very top, the first item won’t disappear under the notch; if they scroll to the very bottom, the last item won’t be obscured by the home indicator. The contentContainerStyle with padding is optional (just to give some inner spacing).

2. Use insets in content container styles: Alternatively, you can use the useSafeAreaInsets hook to get the insets and pass them to the ScrollView's content container:

const insets = useSafeAreaInsets();
...
<ScrollView 
  contentContainerStyle={{ 
    paddingTop: insets.top + 16, 
    paddingBottom: insets.bottom + 16 
  }}>
  {/* ... */}
</ScrollView>

This way, you add padding inside the scroll content itself. On iOS, you can also leverage the contentInsetAdjustmentBehavior="automatic" on ScrollView, which uses the system safe area to adjust scroll insets automatically. However, when using react-native-safe-area-context, it's often easier to just manage it yourself as shown.

3. Use SafeAreaView at top and bottom if needed: In some designs, you might have a fixed header and a scrollable body. You can wrap the header in a SafeAreaView (for the top) and maybe the bottom part of the screen in another SafeAreaView (for bottom) if the scroll view itself should scroll edge-to-edge. But usually one SafeAreaView around the whole screen does the trick.

One thing to watch out for: if you're using a navigation library (like React Navigation), it might handle some safe area for you on certain screens (especially headers or tab bars). But when in doubt, wrapping your screen or using the hook ensures your content is safe.

Safe Areas in Modals and Overlays

Modals (using React Native’s built-in <Modal> or other overlay libraries) present a special case because they are rendered outside the normal React hierarchy. This means they might not inherit the SafeAreaProvider context from your App. If you notice that content in a modal is sneaking under the status bar or notch, you'll need to address safe areas there too.

For a simple fix, you can wrap your modal's content in SafeAreaView just like any other screen:

import { Modal, View, Text, Button } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

function MyModal({ visible, onClose }) {
  return (
    <Modal visible={visible} animationType="slide">
      <SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
        {/* Modal header */}
        <View style={{ padding: 16 }}>
          <Text style={{ fontSize: 20 }}>I am a modal</Text>
          <Button title="Close" onPress={onClose} />
        </View>
        {/* Modal body content... */}
      </SafeAreaView>
    </Modal>
  );
}

This will ensure the modal’s header gets the needed padding. However, since Modal creates a new root, sometimes the SafeAreaView inside it might not know the device insets (if the context isn't provided). If you find that SafeAreaView or useSafeAreaInsets returns 0 inside a Modal, the solution is to include a SafeAreaProvider inside the modal as well. For example:

<Modal visible={visible}>
  <SafeAreaProvider>
    <SafeAreaView style={{ flex: 1 }}>
      {/* ...modal content... */}
    </SafeAreaView>
  </SafeAreaProvider>
</Modal>

Placing a new SafeAreaProvider inside the modal re-calculates the safe areas for that modal's view hierarchy, so it will behave properly. This is a handy trick for any kind of portal or overlay that isn’t wrapped by your main provider. Just be careful to import everything from react-native-safe-area-context within the modal content.

For React Navigation modals (if you use a library's built-in modal screens), they might manage safe areas for you, but with custom modals you’ll likely need to do the above.

Custom Headers and Footers with Safe Area

If you're not using a navigation library’s default header or you have a custom toolbar/footer, you need to manually ensure those are within the safe area.

We already saw an example of a custom header using useSafeAreaInsets. You can also achieve the same using SafeAreaView with an edges prop. The SafeAreaView from the safe-area-context library allows you to specify which edges to apply padding to via the edges prop. By default, edges is ['top','right','bottom','left'] (all edges). But if you only want to pad the top (for a header), or only the bottom (for a footer), you can customize this.

Example – Custom Header with SafeAreaView:

import { SafeAreaView } from 'react-native-safe-area-context';

function HeaderBar() {
  return (
    <SafeAreaView edges={['top', 'left', 'right']} style={styles.headerContainer}>
      <Text style={styles.headerTitle}>My App</Text>
    </SafeAreaView>
  );
}

In this case, we used edges={['top','left','right']} on the header SafeAreaView. That will apply safe area padding to the top and horizontal edges, but not the bottom. This is useful because maybe the header has a fixed height and we only need the top inset. We included left and right just in case of any horizontal safe area (on iPads or landscape mode, sometimes there's a slight inset).

For a footer, you could do similarly:

function BottomToolbar() {
  return (
    <SafeAreaView edges={['bottom', 'left', 'right']} style={styles.footer}>
      <Text>© 2025 MyCompany</Text>
    </SafeAreaView>
  );
}

This would pad only the bottom (and a tad on sides) to keep the content above the home indicator or navigation bar.

Using edges gives you fine control. For instance, if you have a full-screen image that you want to extend into the notch area (for a cool immersive effect), you might wrap the rest of your UI in a SafeAreaView but exclude the top edge so that the image can bleed to the top. Or you could have one SafeAreaView covering the whole screen but pass edges={['left','right','bottom']} so that it ignores the top inset (letting something else handle it).

The key is to think about which parts of your layout should respect which safe boundaries. Most often it's just top and bottom that matter, but edges lets you customize as needed.

Best Practices and Common Pitfalls

Now that we've covered the main APIs, let's summarize some best practices and watch-outs:

By keeping these tips in mind, you'll avoid common pitfalls (like content jumping or blank spaces) and ensure a consistent layout.

Using safe areas vs. manual padding. The left image shows an app that simply added fixed white padding around the content to avoid notches – it avoids overlap but wastes a ton of usable space (notice the giant white bars at top and bottom). The right image shows the app using dynamic safe area insets instead, allowing the background image to extend fully to the edges while still keeping interactive content out of the notch and home indicator areas. The safe area approach maximizes screen usage without hiding any content.

Conclusion

Dealing with notches, status bars, and home indicators might seem tricky at first, but with React Native and Expo's safe area tools it's actually pretty straightforward. We just need to remember to wrap our app with the context provider, and then use SafeAreaView or useSafeAreaInsets to adjust our layouts. In this post, we covered how to keep scroll views, modals, headers, and more within the safe zone.

By applying these techniques, your app will look great on an iPhone 14 Pro, a Pixel 7, an old Android with no notch, and everything in between. No more UI elements hiding in the dreaded notch abyss! 🎉

Happy coding, and may your layouts always stay in the safe area!