🛡️ SafeAreaView: Respecting Device Boundaries
Ensuring your content avoids notches, status bars, and home indicators
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Understand what safe areas are and why they matter
- Use React Native's built-in SafeAreaView component
- Work with react-native-safe-area-context for more control
- Apply safe area insets selectively to specific edges
- Handle safe areas in different screen contexts (modals, tabs)
- Create layouts that work across all device types
⏱️ Estimated Time: 20-30 minutes
📑 In This Lesson
What Are Safe Areas?
Modern mobile devices have various hardware elements that intrude into the screen area: notches, Dynamic Island, rounded corners, status bars, and home indicators. The safe area is the portion of the screen where your content won't be obscured by these elements.
📖 Key Concept
Safe areas define the region of the screen that's guaranteed to be fully visible and interactive, avoiding hardware obstructions and system UI elements.
Different devices have different safe area insets based on their hardware
The Problem Without Safe Areas
// ❌ Content hidden behind notch and home indicator
export default function App() {
return (
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 24 }}>Welcome!</Text>
{/* This text might be hidden behind the notch! */}
</View>
);
}
Why This Matters
📱 Device Variety
iPhones have notches or Dynamic Island. Some Android phones have camera punch-holes. Insets vary by device.
🔄 Orientation Changes
Safe areas change when rotating between portrait and landscape modes.
🎯 Touch Targets
Buttons near edges might be unreachable if they're under the home indicator area.
👁️ Content Visibility
Important text or images shouldn't be obscured by hardware elements.
Basic SafeAreaView
React Native includes a built-in SafeAreaView component that automatically adds padding to avoid device boundaries.
Simple Usage
import { SafeAreaView, View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Welcome!</Text>
<Text>This content respects the safe area.</Text>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
⚠️ Limitations of Built-in SafeAreaView
- iOS only — On Android, it behaves like a regular View
- All-or-nothing — Can't selectively apply insets to specific edges
- No access to inset values — Can't use the numbers for calculations
For most apps, you'll want to use react-native-safe-area-context instead.
Typical App Structure
// Basic structure with SafeAreaView at root
import { SafeAreaView, ScrollView, View, Text } from 'react-native';
export default function HomeScreen() {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#f5f5f5' }}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>My App</Text>
</View>
{/* Scrollable Content */}
<ScrollView contentContainerStyle={styles.content}>
{/* Your content here */}
</ScrollView>
{/* Footer/Tab Bar would go here */}
</SafeAreaView>
);
}
react-native-safe-area-context
The react-native-safe-area-context library is the recommended way to handle safe areas. It works on both iOS and Android, provides access to inset values, and offers more control.
Installation
# With Expo
npx expo install react-native-safe-area-context
# With bare React Native
npm install react-native-safe-area-context
# Then run pod install for iOS
Setup with SafeAreaProvider
Wrap your app with SafeAreaProvider at the root:
// App.tsx
import { SafeAreaProvider } from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaProvider>
<Navigation />
</SafeAreaProvider>
);
}
✅ Expo Projects
If you're using Expo Router, the SafeAreaProvider is already set up for you! You can use the safe area hooks and components directly.
SafeAreaView from the Library
import { SafeAreaView } from 'react-native-safe-area-context';
function HomeScreen() {
return (
<SafeAreaView style={{ flex: 1 }}>
{/* Content is automatically padded on all edges */}
<Text>Safe content</Text>
</SafeAreaView>
);
}
Using the useSafeAreaInsets Hook
For more control, use the useSafeAreaInsets hook to get the actual inset values:
import { View, Text, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function CustomHeader() {
const insets = useSafeAreaInsets();
return (
<View style={[
styles.header,
{ paddingTop: insets.top + 10 } // Safe area + extra padding
]}>
<Text style={styles.title}>My App</Text>
</View>
);
}
const styles = StyleSheet.create({
header: {
backgroundColor: '#2196F3',
paddingHorizontal: 16,
paddingBottom: 10,
},
title: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
},
});
Inset Values
The useSafeAreaInsets hook returns an object with four values:
const insets = useSafeAreaInsets();
// insets = {
// top: 47, // Status bar / notch
// right: 0, // Usually 0 in portrait
// bottom: 34, // Home indicator
// left: 0, // Usually 0 in portrait
// }
// In landscape, left/right might have values for notch
The four inset values define padding needed on each edge
Selective Insets
Often you don't want safe area padding on all edges. For example, a screen with a tab bar already handles the bottom inset, so you only need the top.
Using the edges Prop
import { SafeAreaView } from 'react-native-safe-area-context';
// Only apply safe area to top
<SafeAreaView edges={['top']} style={{ flex: 1 }}>
{/* Content */}
</SafeAreaView>
// Apply to top and bottom only
<SafeAreaView edges={['top', 'bottom']} style={{ flex: 1 }}>
{/* Content */}
</SafeAreaView>
// All edges except bottom (for screens with tab bar)
<SafeAreaView edges={['top', 'left', 'right']} style={{ flex: 1 }}>
{/* Content */}
</SafeAreaView>
Common Edge Configurations
| Screen Type | Edges | Reason |
|---|---|---|
| Root screen (no nav) | ['top', 'bottom', 'left', 'right'] |
Need all insets |
| Screen with header | ['bottom'] or [] |
Header handles top |
| Screen with tab bar | ['top'] |
Tab bar handles bottom |
| Screen with both | [] |
Navigation handles both |
| Modal (full screen) | ['top', 'bottom'] |
Usually portrait only |
Manual Inset Application
Using the hook gives you full control over which insets to apply and how:
import { View, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function ScreenWithTabBar() {
const insets = useSafeAreaInsets();
return (
<View style={[
styles.container,
{
// Only apply top inset - tab bar handles bottom
paddingTop: insets.top,
// In landscape, apply left/right for notch
paddingLeft: insets.left,
paddingRight: insets.right,
}
]}>
{/* Content */}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
Insets in ScrollView
For ScrollView, you can apply insets to the content area instead of the container:
import { ScrollView } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function ScrollableScreen() {
const insets = useSafeAreaInsets();
return (
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{
paddingTop: insets.top,
paddingBottom: insets.bottom + 20, // Extra padding at bottom
paddingHorizontal: 16,
}}
>
{/* Scrollable content */}
</ScrollView>
);
}
Common Patterns
Let's look at real-world patterns for handling safe areas in different scenarios.
Full-Screen Background with Safe Content
When you want a background color or image to extend edge-to-edge, but content stays in the safe area:
import { View, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function FullScreenWithSafeContent() {
const insets = useSafeAreaInsets();
return (
<View style={styles.container}>
{/* Background extends to edges */}
<View style={[
styles.content,
{
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: insets.left,
paddingRight: insets.right,
}
]}>
{/* Content stays in safe area */}
<Text style={styles.title}>Welcome</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#667eea', // Extends to all edges
},
content: {
flex: 1,
},
title: {
color: 'white',
fontSize: 32,
},
});
Custom Header Component
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface HeaderProps {
title: string;
onBack?: () => void;
}
function Header({ title, onBack }: HeaderProps) {
const insets = useSafeAreaInsets();
return (
<View style={[
styles.header,
{ paddingTop: insets.top }
]}>
<View style={styles.headerContent}>
{onBack && (
<Pressable onPress={onBack} style={styles.backButton}>
<Text style={styles.backText}>← Back</Text>
</Pressable>
)}
<Text style={styles.title}>{title}</Text>
<View style={styles.placeholder} />
</View>
</View>
);
}
const styles = StyleSheet.create({
header: {
backgroundColor: '#2196F3',
},
headerContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
height: 56,
paddingHorizontal: 16,
},
backButton: {
width: 60,
},
backText: {
color: 'white',
fontSize: 16,
},
title: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
placeholder: {
width: 60,
},
});
Bottom Action Bar
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function BottomActionBar() {
const insets = useSafeAreaInsets();
return (
<View style={[
styles.container,
{ paddingBottom: Math.max(insets.bottom, 16) }
]}>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>Add to Cart — $99</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
paddingHorizontal: 16,
paddingTop: 12,
},
button: {
backgroundColor: '#2196F3',
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
💡 Math.max for Minimum Padding
Use Math.max(insets.bottom, 16) to ensure at least some padding even on devices with no bottom inset. This prevents content from sitting flush against the edge.
Modal Screens
Modals often need their own safe area handling:
import { View, Modal, StyleSheet } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
function FullScreenModal({ visible, onClose, children }) {
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="fullScreen"
>
<SafeAreaView style={styles.modal} edges={['top', 'bottom']}>
<View style={styles.header}>
<Pressable onPress={onClose}>
<Text>Close</Text>
</Pressable>
</View>
<View style={styles.content}>
{children}
</View>
</SafeAreaView>
</Modal>
);
}
const styles = StyleSheet.create({
modal: {
flex: 1,
backgroundColor: 'white',
},
header: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
content: {
flex: 1,
},
});
Landscape Considerations
In landscape mode, the notch may be on the left or right side:
import { View, StyleSheet, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function LandscapeAwareScreen() {
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
return (
<View style={[
styles.container,
{
paddingTop: insets.top,
paddingBottom: insets.bottom,
// In landscape, notch creates left or right inset
paddingLeft: Math.max(insets.left, 16),
paddingRight: Math.max(insets.right, 16),
}
]}>
{/* Content */}
</View>
);
}
flowchart TD
A[Need Safe Area?] --> B{Using Navigation
React Navigation / Expo Router?}
B -->|Yes| C{Screen Type?}
B -->|No| D[Wrap with SafeAreaView
edges=all]
C -->|Stack screen| E[Header handles top
May need bottom for content]
C -->|Tab screen| F[Tab bar handles bottom
Header handles top]
C -->|Modal| G[Usually need top + bottom
Check presentationStyle]
E --> H{Has scrollable content?}
F --> H
G --> H
H -->|Yes| I[Apply insets to
contentContainerStyle]
H -->|No| J[Apply insets to
container View]
style D fill:#e8f5e9,stroke:#4caf50
style I fill:#e3f2fd,stroke:#1976d2
style J fill:#e3f2fd,stroke:#1976d2
Decision tree for applying safe area insets in different contexts
Hands-On Exercises
Practice makes perfect! These exercises will help you master safe areas.
Exercise 1: Simple Safe Screen
Goal: Create a screen that displays content safely on any device.
Requirements:
- Use react-native-safe-area-context's SafeAreaView
- Display a title "Welcome" at the top
- Display some body text below
- Add a button at the bottom that stays above the home indicator
💡 Hint
Use flex: 1 on the SafeAreaView and justifyContent: 'space-between' to push the button to the bottom.
✅ Solution
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function SafeScreen() {
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<View>
<Text style={styles.title}>Welcome</Text>
<Text style={styles.body}>
This content is safely positioned away from the notch
and home indicator on all devices.
</Text>
</View>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>Get Started</Text>
</Pressable>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
flex: 1,
justifyContent: 'space-between',
padding: 20,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#333',
marginBottom: 12,
},
body: {
fontSize: 16,
color: '#666',
lineHeight: 24,
},
button: {
backgroundColor: '#2196F3',
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
Exercise 2: Custom Header with Insets
Goal: Create a custom header that uses useSafeAreaInsets.
Requirements:
- Header with colored background that extends under the status bar
- Title text positioned below the status bar area
- Use useSafeAreaInsets to get the correct top padding
- Fixed height content area (56px) plus the inset
💡 Hint
Apply paddingTop: insets.top to the header container, then add your fixed-height content below that.
✅ Solution
import { View, Text, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function CustomHeaderScreen() {
const insets = useSafeAreaInsets();
return (
<View style={styles.screen}>
{/* Header with safe area */}
<View style={[
styles.header,
{ paddingTop: insets.top }
]}>
<View style={styles.headerContent}>
<Text style={styles.headerTitle}>My Custom Header</Text>
</View>
</View>
{/* Body content */}
<View style={styles.body}>
<Text style={styles.bodyText}>
The header background extends under the status bar,
but the title is positioned safely below it.
</Text>
<Text style={styles.insetInfo}>
Top inset: {insets.top}px
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#fff',
},
header: {
backgroundColor: '#667eea',
},
headerContent: {
height: 56,
justifyContent: 'center',
alignItems: 'center',
},
headerTitle: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
body: {
flex: 1,
padding: 20,
},
bodyText: {
fontSize: 16,
color: '#333',
lineHeight: 24,
marginBottom: 20,
},
insetInfo: {
fontSize: 14,
color: '#666',
fontStyle: 'italic',
},
});
Exercise 3: Bottom Sheet with Safe Bottom
Goal: Create a simulated bottom sheet that respects the home indicator.
Requirements:
- A View positioned at the bottom of the screen
- Content area with a title and some options
- Bottom padding that accounts for the home indicator
- Use Math.max to ensure minimum padding even without inset
💡 Hint
Use absolute positioning to place the sheet at the bottom, and apply Math.max(insets.bottom, 20) for the bottom padding.
✅ Solution
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function BottomSheetDemo() {
const insets = useSafeAreaInsets();
return (
<View style={styles.container}>
<Text style={styles.backgroundText}>Main Content Area</Text>
{/* Bottom Sheet */}
<View style={[
styles.bottomSheet,
{ paddingBottom: Math.max(insets.bottom, 20) }
]}>
<View style={styles.handle} />
<Text style={styles.sheetTitle}>Quick Actions</Text>
{['Share', 'Save', 'Edit', 'Delete'].map((action) => (
<Pressable key={action} style={styles.option}>
<Text style={styles.optionText}>{action}</Text>
</Pressable>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f0f0f0',
},
backgroundText: {
textAlign: 'center',
marginTop: 100,
fontSize: 18,
color: '#999',
},
bottomSheet: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 12,
paddingHorizontal: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 10,
elevation: 10,
},
handle: {
width: 40,
height: 4,
backgroundColor: '#ddd',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 16,
},
sheetTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 16,
color: '#333',
},
option: {
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
optionText: {
fontSize: 16,
color: '#333',
},
});
Challenge: Adaptive Layout
🏆 Bonus Challenge
Goal: Create a layout that adapts to both portrait and landscape orientations with proper safe area handling.
Features:
- Display different layouts for portrait vs landscape
- Show the current inset values on screen
- Apply insets correctly in both orientations
- Use useWindowDimensions to detect orientation
✅ Solution
import { View, Text, StyleSheet, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function AdaptiveLayout() {
const insets = useSafeAreaInsets();
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
return (
<View style={[
styles.container,
{
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: Math.max(insets.left, 16),
paddingRight: Math.max(insets.right, 16),
}
]}>
<Text style={styles.title}>
{isLandscape ? '🌄 Landscape' : '📱 Portrait'}
</Text>
<View style={[
styles.infoBox,
isLandscape && styles.infoBoxLandscape
]}>
<Text style={styles.label}>Safe Area Insets:</Text>
<View style={[
styles.insetGrid,
isLandscape && styles.insetGridLandscape
]}>
<View style={styles.insetItem}>
<Text style={styles.insetLabel}>Top</Text>
<Text style={styles.insetValue}>{insets.top}px</Text>
</View>
<View style={styles.insetItem}>
<Text style={styles.insetLabel}>Right</Text>
<Text style={styles.insetValue}>{insets.right}px</Text>
</View>
<View style={styles.insetItem}>
<Text style={styles.insetLabel}>Bottom</Text>
<Text style={styles.insetValue}>{insets.bottom}px</Text>
</View>
<View style={styles.insetItem}>
<Text style={styles.insetLabel}>Left</Text>
<Text style={styles.insetValue}>{insets.left}px</Text>
</View>
</View>
</View>
<Text style={styles.dimensions}>
Screen: {Math.round(width)} × {Math.round(height)}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#667eea',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: 'white',
textAlign: 'center',
marginVertical: 20,
},
infoBox: {
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: 16,
padding: 20,
margin: 16,
},
infoBoxLandscape: {
flexDirection: 'row',
alignItems: 'center',
},
label: {
color: 'white',
fontSize: 16,
fontWeight: '600',
marginBottom: 16,
},
insetGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
insetGridLandscape: {
flex: 1,
marginLeft: 20,
},
insetItem: {
width: '48%',
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: 8,
padding: 12,
marginBottom: 8,
alignItems: 'center',
},
insetLabel: {
color: 'rgba(255,255,255,0.8)',
fontSize: 12,
marginBottom: 4,
},
insetValue: {
color: 'white',
fontSize: 20,
fontWeight: 'bold',
},
dimensions: {
color: 'rgba(255,255,255,0.7)',
textAlign: 'center',
marginTop: 20,
},
});
Summary
🎉 Key Takeaways
- Safe areas protect content from notches, status bars, and home indicators
- Built-in SafeAreaView is iOS-only and limited in control
- react-native-safe-area-context is the recommended solution for cross-platform
- SafeAreaProvider must wrap your app (Expo Router includes it automatically)
- useSafeAreaInsets hook gives you the actual inset values for calculations
- edges prop lets you apply insets selectively (top, bottom, left, right)
- Math.max(insets.X, 16) ensures minimum padding even without insets
- Navigation libraries often handle safe areas — don't double-apply!
Quick Reference
// Setup (App.tsx)
import { SafeAreaProvider } from 'react-native-safe-area-context';
<SafeAreaProvider>{children}</SafeAreaProvider>
// Simple usage
import { SafeAreaView } from 'react-native-safe-area-context';
<SafeAreaView style={{ flex: 1 }}>{content}</SafeAreaView>
// Selective edges
<SafeAreaView edges={['top', 'left', 'right']}>
// Hook for manual control
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const insets = useSafeAreaInsets();
// insets.top, insets.right, insets.bottom, insets.left
// Common pattern
<View style={{
paddingTop: insets.top,
paddingBottom: Math.max(insets.bottom, 16)
}}>
🚀 What's Next?
Now that you can properly position content on any device, we'll explore Pressable — the modern way to handle touch interactions in React Native.
🛡️ Safe Areas Mastered!
Your apps will now look polished on every device — from the oldest iPhone SE to the newest iPhone with Dynamic Island. No more content hiding behind notches!