react-navigation
Provides React Navigation UI patterns for stacks, tabs, drawers etc. Use when building navigation UIs with React Navigation, configuring headers, bottom sheets or handling safe areas and insets.
React Navigation
Overview
Guide for building navigation UIs with React Navigation.
This skill only applies to React Navigation 7. The API and patterns may not work with different versions.
API Selection
React Navigation offers two API - object-based Static API and component-based Dynamic API.
- Existing Apps: Check the current navigation setup and follow the same API style when using the references
- New Apps: If the app does not have an existing navigation setup yet, prefer
Static API
When to Apply
Reference this skill when:
- Building navigation UI patterns such as stacks, tabs, drawers, sheets etc.
- Configuring headers and other built-in navigator UI
- Handling safe areas and insets in navigation UI
References
| File | Description |
|---|---|
| stacks.md (references/stacks.md) | Stack based navigation |
| form-sheet.md (references/form-sheet.md) | Bottom sheet and form sheets |
| bottom-tabs.md (references/bottom-tabs.md) | Cross-platform bottom tabs |
| native-bottom-tabs.md (references/native-bottom-tabs.md) | Native bottom tabs |
| material-top-tabs.md (references/material-top-tabs.md) | Swipeable Top tabs |
| drawers.md (references/drawers.md) | Drawer navigation and sidebars |
| header.md (references/header.md) | Configuring headers |
| safe-areas.md (references/safe-areas.md) | Safe-area handling |
Problem -> Skill Mapping
| Problem | Start With |
|---|---|
| Showing screens and modals in a stack | stacks.md (references/stacks.md) |
| Showing bottom sheets or form sheets | form-sheet.md (references/form-sheet.md) |
| Showing screens in bottom tabs or responsive sidebars with web support | bottom-tabs.md (references/bottom-tabs.md) |
| Showing screens in native tabs on iOS & Android | native-bottom-tabs.md (references/native-bottom-tabs.md) |
| Showing content in swipeable top tabs | material-top-tabs.md (references/material-top-tabs.md) |
| Using a drawer or sidebar | drawers.md (references/drawers.md) |
| Configuring the header in bottom tab or drawer navigator | header.md (references/header.md) |
| Handling safe-area such as status bar, header insets, tab bar insets etc. | safe-areas.md (references/safe-areas.md) |
SKILL.md
SKILL.md holds the skill's instructions; it is edited on the Instructions tab.
agents/openai.yaml
interface:
display_name: "React Navigation"
short_description: "Build React Navigation 7 interfaces"
default_prompt: "Use $react-navigation to build or review a React Navigation 7 interface with the appropriate navigator, header, sheet, and safe-area patterns."
references/bottom-tabs.md
title: Bottom Tabs
impact: HIGH
tags: react-navigation, bottom-tabs, tab-bar, icons, blur, sidebar, header
Skill: Bottom Tabs
Description
Use createBottomTabNavigator for cross-platform tab navigation with a tab bar that can stay at the bottom on smaller screens and move to the side on larger layouts if the app needs web support.
Conditionally use native-bottom-tabs.md (./native-bottom-tabs.md) on Android and iOS for a native tab bar.
When to Use
- Building primary app destinations behind a tab bar
- The app needs to run on web and mobile
Basic Example
Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}Common Features
Displaying Icons
Use tabBarIcon in screenOptions or per-screen options. Optionally use tabBarActiveTintColor and tabBarInactiveTintColor to change the color passed to the icon.
Static API
import Ionicons from '@expo/vector-icons/Ionicons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screenOptions: ({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
switch (route.name) {
case 'Home':
iconName = focused ? 'home' : 'home-outline';
break;
default:
iconName = focused ? 'settings' : 'settings-outline';
break;
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
}),
screens: {
Home: HomeScreen,
Settings: SettingsScreen,
},
});Dynamic API
import Ionicons from '@expo/vector-icons/Ionicons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
switch (route.name) {
case 'Home':
iconName = focused ? 'home' : 'home-outline';
break;
default:
iconName = focused ? 'settings' : 'settings-outline';
break;
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
}The tabBarIcon option can render any React element. Choose between the icon libraries that best fit the app's design and environment:
@expo/vector-iconsif Expo SDK is configured in the appreact-native-vector-iconsfor an app using React Native Community CLI<Image>for local images
Scroll to Top on Tab Press
Use useScrollToTop on a scrollable ref inside a screen in the tab navigator to automatically scroll to the top when the tab is pressed while already focused.
import * as React from 'react';
import { ScrollView } from 'react-native';
import { useScrollToTop } from '@react-navigation/native';
function FeedScreen() {
const ref = React.useRef(null);
useScrollToTop(ref);
return <ScrollView ref={ref}>{/* content */}</ScrollView>;
}Custom Background Such as Blur
Use tabBarBackground for custom chrome such as blur effects, image, or gradient backgrounds. When the background should show through the tab bar, set tabBarStyle: { position: 'absolute' } and use useBottomTabBarHeight inside the screens to pad the content.
Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
const AppTabs = createBottomTabNavigator({
screenOptions: {
tabBarStyle: { position: 'absolute' },
tabBarBackground: () => (
<BlurView tint="light" intensity={100} style={StyleSheet.absoluteFill} />
),
},
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarStyle: { position: 'absolute' },
tabBarBackground: () => (
<BlurView
tint="light"
intensity={100}
style={StyleSheet.absoluteFill}
/>
),
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}Custom Tab Bar
Use the tabBar option if a custom design is needed. Use the state prop to access the list of screens and descriptors to access the options for each screen. Use the navigation prop passed to the tab bar for navigation instead of useNavigation.
import { Text, View } from 'react-native';
import { useLinkBuilder } from '@react-navigation/native';
import { PlatformPressable } from '@react-navigation/elements';
function MyTabBar({ state, descriptors, navigation }) {
const { buildHref } = useLinkBuilder();
return (
<View style={{ flexDirection: 'row' }}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = state.index === index;
return (
<PlatformPressable
key={route.key}
href={buildHref(route.name, route.params)}
onPress={() => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name, route.params);
}
}}
onLongPress={() =>
navigation.emit({
type: 'tabLongPress',
target: route.key,
})
}
style={{ flex: 1, alignItems: 'center', paddingVertical: 12 }}
>
<Text style={{ opacity: isFocused ? 1 : 0.6 }}>{label}</Text>
</PlatformPressable>
);
})}
</View>
);
}Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
tabBar: (props) => <MyTabBar {...props} />,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator tabBar={(props) => <MyTabBar {...props} />}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}Sidebar
Set tabBarPosition to left or right to render the tab bar as a sidebar. Choose the position based on the screen width for responsive layouts.
Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screenOptions: {
tabBarPosition: 'left',
},
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
}).with(({ Navigator }) => {
const dimensions = useWindowDimensions();
return (
<Navigator
screenOptions={{
tabBarPosition: dimensions.width >= 768 ? 'left' : 'bottom',
}}
/>
);
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useWindowDimensions } from 'react-native';
const Tab = createBottomTabNavigator();
function AppTabs() {
const dimensions = useWindowDimensions();
return (
<Tab.Navigator
screenOptions={{
tabBarPosition: dimensions.width >= 768 ? 'left' : 'bottom',
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}For a compact sidebar, use tabBarVariant: 'material' together with tabBarLabelPosition: 'below-icon'.
Customizing Header
Bottom tabs show a header by default. Use header.md (./header.md) for common customization patterns.
A custom header can be shown with the header option if a custom design is needed.
Static API
import { getHeaderTitle } from '@react-navigation/elements';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screens: {
Home: {
screen: HomeScreen,
options: {
headerStyle: {
height: 80,
},
header: ({ route, options }) => {
const title = getHeaderTitle(options, route.name);
return <MyHeader title={title} style={options.headerStyle} />;
},
},
},
},
});Dynamic API
import { getHeaderTitle } from '@react-navigation/elements';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
headerStyle: {
height: 80,
},
header: ({ route, options }) => {
const title = getHeaderTitle(options, route.name);
return <MyHeader title={title} style={options.headerStyle} />;
},
}}
/>
</Tab.Navigator>
);
}- Set
headerShown: falseto hide the header. - If a custom header uses a non-default height, set
headerStyle: { height: ... }explicitly to avoid measurement glitches.
Hiding Tab Bar on Certain Screens
The tab bar is shown on all screens in the tab navigator. To hide the tab bar on certain screens, put those screens in a parent stack navigator instead.
Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const HomeTabs = createBottomTabNavigator({
screens: {
Home: HomeScreen,
Feed: FeedScreen,
Notifications: NotificationsScreen,
},
});
const AppStack = createNativeStackNavigator({
screens: {
Main: {
screen: HomeTabs,
options: {
headerShown: false,
},
},
Profile: ProfileScreen,
Settings: SettingsScreen,
},
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Notifications" component={NotificationsScreen} />
</Tab.Navigator>
);
}
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="Main"
component={HomeTabs}
options={{
headerShown: false,
}}
/>
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>
);
}- Prefer this structure over trying to hide the parent tab bar from screens inside the tab navigator as it can lead to layout glitches.
- Use stacks.md (./stacks.md) for stack-specific options on the full-screen routes.
Canonical Docs
Related Skills
- header.md (./header.md)
- native-bottom-tabs.md (./native-bottom-tabs.md)
- material-top-tabs.md (./material-top-tabs.md)
- safe-areas.md (./safe-areas.md)
references/drawers.md
title: Drawer Navigator
impact: HIGH
tags: react-navigation, drawer, sidebar, master-detail, header, responsive-layout
Skill: Drawer Navigator
Description
Use createDrawerNavigator for a navigation drawer or sidebar that switches between app sections. If using a drawer for displaying content instead of navigation, use react-native-drawer-layout instead.
When to Use
- Building app sections behind a drawer or sidebar
- Showing a permanent sidebar on larger screens and a drawer on smaller screens
- Using the drawer as the master pane in a master-detail layout
Prerequisites
Install and configure react-native-gesture-handler, react-native-reanimated, and react-native-worklets for drawer navigator to work on native platforms.
Basic Example
Static API
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
return (
<Drawer.Navigator>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Profile" component={ProfileScreen} />
</Drawer.Navigator>
);
}Opening and Closing the Drawer
Use navigation.openDrawer(), navigation.closeDrawer(), and navigation.toggleDrawer() inside drawer screens to open or close the drawer programmatically.
import { Button, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
function HomeScreen() {
const navigation = useNavigation();
return (
<View>
<Button title="Open drawer" onPress={() => navigation.openDrawer()} />
<Button title="Close drawer" onPress={() => navigation.closeDrawer()} />
<Button title="Toggle drawer" onPress={() => navigation.toggleDrawer()} />
</View>
);
}Nesting Drawers
When nesting a drawer inside another drawer, use react-native-drawer-layout for the outer drawer and keep Drawer navigator for the inner navigation drawer.
import * as React from 'react';
import { Text } from 'react-native';
import { Drawer } from 'react-native-drawer-layout';
function App() {
const [rightDrawerOpen, setRightDrawerOpen] = React.useState(false);
return (
<Drawer
open={rightDrawerOpen}
onOpen={() => setRightDrawerOpen(true)}
onClose={() => setRightDrawerOpen(false)}
drawerPosition="right"
renderDrawerContent={() => <Text>Right drawer content</Text>}
>
<AppDrawer />
</Drawer>
);
}Use React context to expose methods for opening or closing the outer drawer from nested screens.
Common Features
Displaying Icons
Use drawerIcon in screenOptions or per-screen options. drawerActiveTintColor and drawerInactiveTintColor control the color passed to the icon and label.
Static API
import Ionicons from '@expo/vector-icons/Ionicons';
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
screenOptions: ({ route }) => ({
drawerIcon: ({ color, size }) => {
let iconName;
switch (route.name) {
case 'Home':
iconName = 'home-outline';
break;
default:
iconName = 'person-outline';
break;
}
return <Ionicons name={iconName} size={size} color={color} />;
},
drawerActiveTintColor: 'tomato',
drawerInactiveTintColor: 'gray',
}),
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import Ionicons from '@expo/vector-icons/Ionicons';
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
return (
<Drawer.Navigator
screenOptions={({ route }) => ({
drawerIcon: ({ color, size }) => {
let iconName;
switch (route.name) {
case 'Home':
iconName = 'home-outline';
break;
default:
iconName = 'person-outline';
break;
}
return <Ionicons name={iconName} size={size} color={color} />;
},
drawerActiveTintColor: 'tomato',
drawerInactiveTintColor: 'gray',
})}
>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Profile" component={ProfileScreen} />
</Drawer.Navigator>
);
}The drawerIcon option can render any React element. Choose between the icon libraries that best fit the app's design and environment:
@expo/vector-iconsif Expo SDK is configured in the appreact-native-vector-iconsfor an app using React Native Community CLI<Image>for local images
Custom Drawer Content
Use drawerContent to customize the content of drawer, such as adding a header, footer, or custom actions, or replacing with a custom design.
Use DrawerContentScrollView to automatically adjust insets and DrawerItemList to keep default design. Use the navigation prop passed to the custom drawer content instead of useNavigation.
import {
createDrawerNavigator,
DrawerContentScrollView,
DrawerItem,
DrawerItemList,
} from '@react-navigation/drawer';
function CustomDrawerContent(props) {
return (
<DrawerContentScrollView {...props}>
<DrawerItemList {...props} />
<DrawerItem
label="Help"
onPress={() => props.navigation.navigate('Help')}
/>
</DrawerContentScrollView>
);
}Replace the content with own element if a different design is needed. Use the state prop to access the list of screens and descriptors to access the options for each screen.
Static API
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
drawerContent: (props) => <CustomDrawerContent {...props} />,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
Help: HelpScreen,
},
});Dynamic API
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
return (
<Drawer.Navigator
drawerContent={(props) => <CustomDrawerContent {...props} />}
>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Profile" component={ProfileScreen} />
<Drawer.Screen name="Help" component={HelpScreen} />
</Drawer.Navigator>
);
}Types of Drawer
Use drawerType to choose how the drawer behaves.
Choose between front, back, and slide on smaller screens. Use permanent for a sidebar on larger screens. Use useWindowDimensions to conditionally set the type based on screen width for responsive layouts.
Static API
import { useWindowDimensions } from 'react-native';
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
}).with(({ Navigator }) => {
const { width } = useWindowDimensions();
return (
<Navigator
screenOptions={{
drawerType: width >= 768 ? 'permanent' : 'front',
}}
/>
);
});Dynamic API
import { useWindowDimensions } from 'react-native';
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
const { width } = useWindowDimensions();
return (
<Drawer.Navigator
screenOptions={{
drawerType: width >= 768 ? 'permanent' : 'front',
}}
>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Profile" component={ProfileScreen} />
</Drawer.Navigator>
);
}Drawer Position
Use drawerPosition to place the drawer on the left or right side. The default is left in LTR languages and right in RTL languages.
Static API
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
screenOptions: {
drawerPosition: 'right',
},
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
return (
<Drawer.Navigator
screenOptions={{
drawerPosition: 'right',
}}
>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Profile" component={ProfileScreen} />
</Drawer.Navigator>
);
}Master Detail Layout
Combine defaultStatus: 'open' with drawerType: 'back' when the drawer should start open and behave like the master pane behind the detail screen.
Static API
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
defaultStatus: 'open',
screenOptions: {
drawerType: 'back',
drawerStyle: { width: '100%' },
overlayColor: 'transparent',
},
screens: {
Inbox: InboxScreen,
Message: MessageScreen,
},
});Dynamic API
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
return (
<Drawer.Navigator
defaultStatus="open"
screenOptions={{
drawerType: 'back',
drawerStyle: { width: '100%' },
overlayColor: 'transparent',
}}
>
<Drawer.Screen name="Inbox" component={InboxScreen} />
<Drawer.Screen name="Message" component={MessageScreen} />
</Drawer.Navigator>
);
}With defaultStatus: 'open', the drawer's default state is open. So if drawer is closed, the back button will open it.
Customizing Header
Drawer screens show a header by default. Use header.md (./header.md) for common customization patterns.
A custom header can be shown with the header option if a custom design is needed.
Static API
import { getHeaderTitle } from '@react-navigation/elements';
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
screens: {
Home: {
screen: HomeScreen,
options: {
headerStyle: {
height: 80,
},
header: ({ route, options }) => {
const title = getHeaderTitle(options, route.name);
return <MyHeader title={title} style={options.headerStyle} />;
},
},
},
},
});Dynamic API
import { getHeaderTitle } from '@react-navigation/elements';
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
return (
<Drawer.Navigator>
<Drawer.Screen
name="Home"
component={HomeScreen}
options={{
headerStyle: {
height: 80,
},
header: ({ route, options }) => {
const title = getHeaderTitle(options, route.name);
return <MyHeader title={title} style={options.headerStyle} />;
},
}}
/>
</Drawer.Navigator>
);
}- Set
headerShown: falseto hide the header. - If a custom header uses a non-default height, set
headerStyle: { height: ... }explicitly to avoid measurement glitches.
Notes
- Swipe gestures and gesture-handler customization are not supported on web.
- If a drawer is nested under a stack or tab navigator, it renders below that parent navigator's header or tab bar.
Canonical Docs
Related Skills
- header.md (./header.md)
- bottom-tabs.md (./bottom-tabs.md)
- stacks.md (./stacks.md)
- safe-areas.md (./safe-areas.md)
references/form-sheet.md
title: Form Sheets with Native Stack
impact: MEDIUM
tags: react-navigation, native-stack, form-sheet, bottom-sheet, modal, detents
Skill: Form Sheets with Native Stack
Description
Use native-stack presentation: 'formSheet' when a screen should open as a sheet.
When to Use
- Presenting secondary flows such as edit forms, filters, pickers, or lightweight detail screens
- Keeping the current screen visible underneath a native sheet presentation
Basic Example
Static API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
EditProfile: {
screen: EditProfileScreen,
options: {
presentation: 'formSheet',
headerShown: false,
sheetAllowedDetents: 'fitToContents',
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="EditProfile"
component={EditProfileScreen}
options={{
presentation: 'formSheet',
headerShown: false,
sheetAllowedDetents: 'fitToContents',
}}
/>
</Stack.Navigator>
);
}Configuring Sheet Sizes
By default, a form sheet takes the full screen height. Use sheetAllowedDetents to control the heights where the sheet can rest.
- Use
'fitToContents'to size the sheet from its content. - Use an ascending array of fractions such as
[0.25, 0.5, 1]for fixed detents. - On Android, only the first 3 detents are used.
- Use
sheetInitialDetentIndexto choose the opening detent, or'last'for the largest one.
Fit to Contents
sheetResizeAnimationEnabled is Android only and controls the default resize animation when using 'fitToContents'.
Static API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Filters: {
screen: FiltersScreen,
options: {
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Filters"
component={FiltersScreen}
options={{
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
}}
/>
</Stack.Navigator>
);
}On iOS, a top-level flex: 1 content container works with 'fitToContents'.
On Android, 'fitToContents' does not work with a top-level flex: 1 content container. The sheet can disappear and leave only the dimmed backdrop visible.
Fixed Detents and Initial Detent
sheetShouldOverflowTopInset is Android only and changes whether detent fractions are measured against the full stack height or the inset-adjusted height.
Static API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Details: {
screen: DetailsScreen,
options: {
presentation: 'formSheet',
sheetAllowedDetents: [0.25, 0.5, 1],
sheetInitialDetentIndex: 1,
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Details"
component={DetailsScreen}
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.25, 0.5, 1],
sheetInitialDetentIndex: 1,
}}
/>
</Stack.Navigator>
);
}On iOS, fixed detents do not automatically make content fill the sheet height. Use contentStyle.backgroundColor if the uncovered area would otherwise look translucent.
Customizing Appearance
sheetGrabberVisibleis iOS only and shows the grabber.sheetCornerRadiusoverrides the default corner radius.sheetElevationis Android only, adjusts the top-edge shadow, and cannot be changed after mount.sheetLargestUndimmedDetentIndexcontrols when the background stays undimmed:'none'always dims,'last'never dims, and a number keeps the background undimmed up to that detent index. On iOS, the system can still add dimming if the sheet grows beyond that height without a detent change, such as when the keyboard appears.contentStyle.backgroundColorsets an explicit background color for the whole sheet. This is especially useful on iOS with fixed detents, where the content does not automatically fill the sheet height.
Static API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Composer: {
screen: ComposerScreen,
options: {
presentation: 'formSheet',
sheetAllowedDetents: [0.5, 1],
sheetGrabberVisible: true,
sheetCornerRadius: 24,
sheetElevation: 12,
sheetLargestUndimmedDetentIndex: 0,
contentStyle: {
backgroundColor: '#fff',
},
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Composer"
component={ComposerScreen}
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, 1],
sheetGrabberVisible: true,
sheetCornerRadius: 24,
sheetElevation: 12,
sheetLargestUndimmedDetentIndex: 0,
contentStyle: {
backgroundColor: '#fff',
},
}}
/>
</Stack.Navigator>
);
}Scroll Behavior
On iOS, scrolling can expand the sheet to a larger detent by default. sheetExpandsWhenScrolledToEdge is iOS only, and setting it to false prevents that behavior.
For this to work, the ScrollView must be reachable by following the first child at each level from the screen component (aka first descendant chain).
Static API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Comments: {
screen: CommentsScreen,
options: {
presentation: 'formSheet',
sheetAllowedDetents: [0.5, 1],
sheetExpandsWhenScrolledToEdge: false,
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Comments"
component={CommentsScreen}
options={{
presentation: 'formSheet',
sheetAllowedDetents: [0.5, 1],
sheetExpandsWhenScrolledToEdge: false,
}}
/>
</Stack.Navigator>
);
}On Android, nested ScrollView usage may require nestedScrollEnabled, and still does not work when the content is shorter than the scroll view.
Notes
- On Android,
presentation: 'formSheet'screens do not currently support nested stack navigators orheaderShown.
Canonical Docs
Related Skills
- stacks.md (./stacks.md)
- safe-areas.md (./safe-areas.md)
references/header.md
title: Header
impact: MEDIUM
tags: react-navigation, header, title, header-buttons, header-background, react-navigation-elements
Skill: Header
Description
Use this when customizing the header in bottom tabs or drawer which render a JS-based header.
For native stack headers, use stacks.md (./stacks.md) instead.
When to Use
- Styling the built-in header in bottom tabs or drawer screens
- Adding header buttons, custom title content, or a custom background
Basic Example
The shared header options work the same in bottom tabs and drawer. The examples below use bottom tabs.
Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screens: {
Home: {
screen: HomeScreen,
options: {
title: 'Home',
headerStyle: {
backgroundColor: '#fff',
},
headerTintColor: '#111',
headerRight: () => <HeaderAction />,
},
},
},
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
title: 'Home',
headerStyle: {
backgroundColor: '#fff',
},
headerTintColor: '#111',
headerRight: () => <HeaderAction />,
}}
/>
</Tab.Navigator>
);
}- Use
title,headerStyle,headerTintColor,headerTitleStyle, andheaderTitleAlignfor the default header. Put shared config in navigatorscreenOptionsand per-screen overrides inoptions. - Use
headerShown: falsefor screens that contain a navigator - the header from the child navigator should be shown instead.
Common Features
Buttons and Custom Content
Use headerLeft, headerRight, headerTitle, and headerBackground when the default header layout is fine but specific pieces need customization.
Static API
import { Button, Text } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screens: {
Inbox: {
screen: InboxScreen,
options: {
headerLeft: ({ tintColor }) => (
<Button title="Edit" color={tintColor} onPress={() => {}} />
),
headerTitle: ({ tintColor, children }) => (
<Text style={{ color: tintColor, fontSize: 18 }}>{children}</Text>
),
headerRight: ({ tintColor }) => (
<Button title="Done" color={tintColor} onPress={() => {}} />
),
},
},
},
});Dynamic API
import { Button, Text } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Inbox"
component={InboxScreen}
options={{
headerLeft: ({ tintColor }) => (
<Button title="Edit" color={tintColor} onPress={() => {}} />
),
headerTitle: ({ tintColor, children }) => (
<Text style={{ color: tintColor, fontSize: 18 }}>{children}</Text>
),
headerRight: ({ tintColor }) => (
<Button title="Done" color={tintColor} onPress={() => {}} />
),
}}
/>
</Tab.Navigator>
);
}headerLeftandheaderRightreceive props such astintColor,pressColor, andpressOpacity.headerTitleis useful when the title needs custom typography, icons, or extra layout.- Use
headerTitleContainerStyle,headerLeftContainerStyle, andheaderRightContainerStyleto customize the container styles.
Search Bar
Use headerSearchBarOptions to add a search button that expands into a search input in the header. If the search configuration depends on screen state, update it with navigation.setOptions(...).
Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screens: {
Search: {
screen: SearchScreen,
options: {
headerSearchBarOptions: {
placeholder: 'Search',
onChangeText: (text) => {
// Do something
},
},
},
},
},
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Search"
component={SearchScreen}
options={{
headerSearchBarOptions: {
placeholder: 'Search',
onChangeText: (text) => {
// Do something
},
},
}}
/>
</Tab.Navigator>
);
}Translucent Headers
Use headerTransparent: true to let content show underneath the header. Combine it with headerBackground for effects such as a blur, gradient, or image background.
Static API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
const AppTabs = createBottomTabNavigator({
screens: {
Feed: {
screen: FeedScreen,
options: {
headerTransparent: true,
headerBackground: () => (
<BlurView
tint="light"
intensity={100}
style={StyleSheet.absoluteFill}
/>
),
},
},
},
});Dynamic API
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Feed"
component={FeedScreen}
options={{
headerTransparent: true,
headerBackground: () => (
<BlurView
tint="light"
intensity={100}
style={StyleSheet.absoluteFill}
/>
),
}}
/>
</Tab.Navigator>
);
}headerStyledoes not apply whenheaderBackgroundis used. Style the element returned fromheaderBackgroundinstead.- Transparent headers overlap the content below. Add top spacing manually using
useHeaderHeighthook from@react-navigation/elements
Canonical Docs
Related Skills
- bottom-tabs.md (./bottom-tabs.md)
- drawers.md (./drawers.md)
- stacks.md (./stacks.md)
references/material-top-tabs.md
title: Material Top Tabs
impact: MEDIUM
tags: react-navigation, material-top-tabs, swipe, pager-view, lazy, tab-animation
Skill: Material Top Tabs
Description
Use createMaterialTopTabNavigator for swipeable top tabs with a Material-style tab bar.
When to Use
- Building segmented content that should switch by tap or horizontal swipe
- Having a scrollable tab bar at the top of the screen
Prerequisites
Install react-native-pager-view for material top tabs to work on native platforms.
Basic Example
Static API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
},
});Dynamic API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Updates" component={UpdatesScreen} />
</Tab.Navigator>
);
}Common Features
Displaying Icons
Icons are hidden by default. Set tabBarShowIcon: true, then provide tabBarIcon. Optionally use tabBarActiveTintColor and tabBarInactiveTintColor to change the color passed to the icon.
Static API
import Ionicons from '@expo/vector-icons/Ionicons';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
screenOptions: ({ route }) => ({
tabBarShowIcon: true,
tabBarIcon: ({ focused, color }) => {
let iconName;
switch (route.name) {
case 'Feed':
iconName = focused ? 'newspaper' : 'newspaper-outline';
break;
default:
iconName = focused ? 'notifications' : 'notifications-outline';
break;
}
return <Ionicons name={iconName} size={18} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
}),
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
},
});Dynamic API
import Ionicons from '@expo/vector-icons/Ionicons';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarShowIcon: true,
tabBarIcon: ({ focused, color }) => {
let iconName;
switch (route.name) {
case 'Feed':
iconName = focused ? 'newspaper' : 'newspaper-outline';
break;
default:
iconName = focused ? 'notifications' : 'notifications-outline';
break;
}
return <Ionicons name={iconName} size={18} color={color} />;
},
tabBarActiveTintColor: 'tomato',
tabBarInactiveTintColor: 'gray',
})}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Updates" component={UpdatesScreen} />
</Tab.Navigator>
);
}The tabBarIcon option can render any React element. Choose between the icon libraries that best fit the app's design and environment:
@expo/vector-iconsif Expo SDK is configured in the appreact-native-vector-iconsfor an app using React Native Community CLI<Image>for local images
Scrollable Tab Bar
Use tabBarScrollEnabled: true if the list of tabs won't fit on the screen to make the tab bar scrollable. Set a fixed width for each tab with tabBarItemStyle to make the scrollable tab bar work better.
Use tabBarItemStyle with { width: 'auto' } if the tab button width should fit the label instead of being equally divided.
Static API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
screenOptions: {
tabBarScrollEnabled: true,
},
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
Mentions: MentionsScreen,
Saved: SavedScreen,
Archive: ArchiveScreen,
},
});Dynamic API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarScrollEnabled: true,
}}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Updates" component={UpdatesScreen} />
<Tab.Screen name="Mentions" component={MentionsScreen} />
<Tab.Screen name="Saved" component={SavedScreen} />
<Tab.Screen name="Archive" component={ArchiveScreen} />
</Tab.Navigator>
);
}Badges
Use tabBarBadge to render a custom badge element for a tab.
Static API
import { Text, View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
screens: {
Inbox: {
screen: InboxScreen,
options: {
tabBarBadge: () => (
<View
style={{
minWidth: 18,
height: 18,
borderRadius: 9,
backgroundColor: 'tomato',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ color: 'white', fontSize: 12 }}>3</Text>
</View>
),
},
},
Settings: SettingsScreen,
},
});Dynamic API
import { Text, View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Inbox"
component={InboxScreen}
options={{
tabBarBadge: () => (
<View
style={{
minWidth: 18,
height: 18,
borderRadius: 9,
backgroundColor: 'tomato',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text style={{ color: 'white', fontSize: 12 }}>3</Text>
</View>
),
}}
/>
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
}Indicators
Use tabBarIndicatorStyle to customize the indicator:
- Use horizontal margin (
{ marginHorizontal: 16 }) to make the indicator shorter than the tab button width. - Set width (
{ width: 24 }) for a fixed-width indicator, withmarginHorizontal: 'auto'to center it under the tab button. - Don't set
borderRadiusiftabBarItemStylehas'auto'width as it's not supported.
If the default styling options aren't sufficient, use tabBarIndicator to render a custom indicator.
Static API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
screenOptions: {
tabBarIndicatorStyle: {
backgroundColor: '#111827',
height: 3,
marginHorizontal: 16,
},
},
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
},
});Dynamic API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarIndicatorStyle: {
backgroundColor: '#111827',
height: 3,
marginHorizontal: 16,
},
}}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Updates" component={UpdatesScreen} />
</Tab.Navigator>
);
}Custom Tab Bar
Use the tabBar option if a custom design is needed. Use the navigation prop passed to the tab bar instead of useNavigation.
import { Text, View } from 'react-native';
import { useLinkBuilder } from '@react-navigation/native';
import { PlatformPressable } from '@react-navigation/elements';
function MyTabBar({ state, descriptors, navigation }) {
const { buildHref } = useLinkBuilder();
return (
<View style={{ flexDirection: 'row' }}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = state.index === index;
return (
<PlatformPressable
key={route.key}
href={buildHref(route.name, route.params)}
onPress={() => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name, route.params);
}
}}
onLongPress={() =>
navigation.emit({
type: 'tabLongPress',
target: route.key,
})
}
style={{ flex: 1, alignItems: 'center', paddingVertical: 12 }}
>
<Text style={{ opacity: isFocused ? 1 : 0.6 }}>{label}</Text>
</PlatformPressable>
);
})}
</View>
);
}Static API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
tabBar: (props) => <MyTabBar {...props} />,
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
},
});Dynamic API
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator tabBar={(props) => <MyTabBar {...props} />}>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Updates" component={UpdatesScreen} />
</Tab.Navigator>
);
}If the custom tab bar also needs proper web links, use useLinkBuilder when building each tab button.
Lazy Rendering
All screens are mounted immediately by default for smoother swiping. Enable lazy only when rendering tabs upfront is too expensive.
Static API
import { ActivityIndicator, View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
screenOptions: {
lazy: true,
},
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
Mentions: MentionsScreen,
},
});Dynamic API
import { ActivityIndicator, View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator
screenOptions={{
lazy: true,
}}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Updates" component={UpdatesScreen} />
<Tab.Screen name="Mentions" component={MentionsScreen} />
</Tab.Navigator>
);
}Customize lazy loading behavior with lazyPlaceholder to show a custom placeholder before the screen is loaded and lazyPreloadDistance to control how many screens away from the focused screen should be preloaded.
Canonical Docs
references/native-bottom-tabs.md
title: Native Bottom Tabs
impact: HIGH
tags: react-navigation, native-bottom-tabs, native-stack, navigate, search-tab, bottom-accessory, sidebar
Skill: Native Bottom Tabs
Description
Use createNativeBottomTabNavigator for a native tab bar on iOS and Android.
Conditionally use bottom-tabs.md (./bottom-tabs.md) if web support is needed.
When to Use
- Building primary app destinations behind a tab bar
- The app runs on iOS or Android
Basic Example
Static API
import { createNativeBottomTabNavigator } from '@react-navigation/bottom-tabs/unstable';
const AppTabs = createNativeBottomTabNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import { createNativeBottomTabNavigator } from '@react-navigation/bottom-tabs/unstable';
const Tab = createNativeBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}Avoiding Overlap with the Tab Bar
On iOS, content inset is automatically adjusted for scrollable views to avoid overlap with the tab bar.
For non-scrollable content, or on Android, use the experimental SafeAreaView from react-native-screens/experimental to apply padding for the tab bar.
import { SafeAreaView } from 'react-native-screens/experimental';
function ProfileScreen() {
return <SafeAreaView edges={{ bottom: true }}>{/* content */}</SafeAreaView>;
}Common Features
Displaying Icons
Use tabBarIcon in screenOptions or per-screen options. Optionally use tabBarActiveTintColor and tabBarInactiveTintColor to change the color of the icon (support varies based on platform).
Static API
const AppTabs = createNativeBottomTabNavigator({
screens: {
Feed: {
screen: FeedScreen,
options: {
tabBarIcon: Platform.select({
ios: {
type: 'sfSymbol',
name: 'favorites',
},
default: {
type: 'image',
source: require('./assets/feed.png'),
},
}),
},
},
},
});Dynamic API
const Tab = createNativeBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Feed"
component={FeedScreen}
options={{
tabBarIcon: Platform.select({
ios: {
type: 'sfSymbol',
name: 'favorites',
},
default: {
type: 'image',
source: require('./assets/feed.png'),
},
}),
}}
/>
</Tab.Navigator>
);
}The tabBarIcon option supports multiple types of icons:
- SF Symbols on iOS with
type: 'sfSymbol'andname. - Local images with
type: 'image'andsource.
Prefer SF Symbols on iOS, and image icons for other platforms. Use Platform.select to specify different icons per platform if needed.
When using image icons:
- Provide
1x,2x, and3ximage assets (image.png,image@2x.png,image@3x.png) because iOS does not scale tab icons automatically. - Set
tinted: falseon iOS ({ type: 'image', tinted: false, source: require('./assets/feed.png') }) if the image should keep its original colors. Android always tints image icons.
Search Tab on iOS 26+
Use the built-in search system item together with a nested native stack navigator whose focused screen defines headerSearchBarOptions.
Static API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const SearchStack = createNativeStackNavigator({
screens: {
FruitsList: {
screen: FruitsListScreen,
options: {
title: 'Search',
headerSearchBarOptions: {
placeholder: 'Search fruits',
},
},
},
},
});
const AppTabs = createNativeBottomTabNavigator({
screens: {
Search: {
screen: SearchStack,
options: {
tabBarSystemItem: 'search',
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
const Tab = createNativeBottomTabNavigator();
function SearchStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="FruitsList"
component={FruitsListScreen}
options={{
title: 'Search',
headerSearchBarOptions: {
placeholder: 'Search fruits',
},
}}
/>
</Stack.Navigator>
);
}
function AppTabs() {
return (
<Tab.Navigator>
<Tab.Screen
name="Search"
component={SearchStack}
options={{
tabBarSystemItem: 'search',
}}
/>
</Tab.Navigator>
);
}- This behavior is available on iOS 26 and above.
- The tab bar transforms into a search field only when the selected tab renders a nested native stack navigator.
- The focused screen in that nested native stack must define
headerSearchBarOptions.
Bottom Accessory
Use bottomAccessory to render content above the tab bar or inline with the collapsed bar.
Static API
const AppTabs = createNativeBottomTabNavigator({
screenOptions: {
bottomAccessory: ({ placement }) => {
return (
<View style={{ padding: 16 }}>
<Text>Placement: {placement}</Text>
</View>
);
},
},
screens: {
Home: HomeScreen,
},
});Dynamic API
const Tab = createNativeBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator
screenOptions={{
bottomAccessory: ({ placement }) => {
return (
<View style={{ padding: 16 }}>
<Text>Placement: {placement}</Text>
</View>
);
},
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
</Tab.Navigator>
);
}placementisregularabove the bar orinlinewhen the bar is collapsed.- This is supported on iOS 26 and above.
- The accessory renders twice, once per placement, so shared state should live outside the returned component.
Sidebar
Use tabBarControllerMode: 'tabSidebar' to display the native tab controller as a sidebar.
Static API
const AppTabs = createNativeBottomTabNavigator({
screenOptions: {
tabBarControllerMode: 'tabSidebar',
},
screens: {
Home: HomeScreen,
Search: SearchScreen,
},
});Dynamic API
const Tab = createNativeBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarControllerMode: 'tabSidebar',
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
</Tab.Navigator>
);
}autolets the system choose the presentation.tabBarforces the standard tab bar.- Sidebar mode is supported on iOS 18 and above and not on tvOS.
Minimize on Scroll
Use tabBarMinimizeBehavior to let the system collapse the tab bar while scrolling.
Static API
const AppTabs = createNativeBottomTabNavigator({
screenOptions: {
tabBarMinimizeBehavior: 'onScrollDown',
},
screens: {
Home: HomeScreen,
},
});Dynamic API
const Tab = createNativeBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator
screenOptions={{
tabBarMinimizeBehavior: 'onScrollDown',
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
</Tab.Navigator>
);
}- Supported values are
auto,never,onScrollDown, andonScrollUp. - This is supported on iOS 26 and above.
Notes
- React Native 0.79 or above is required. If you're using Expo, SDK 53 or above is required.
- The app must use the latest
react-native-screensalong with the latest version of@react-navigation/bottom-tabsto avoid compatibility issues. - When using Expo, development builds maybe required to support the latest version of
react-native-screens. - Liquid Glass effect on iOS 26+ requires your app to be built with Xcode 26 or above.
- On Android, at most 5 tabs are supported. This is a limitation of the underlying native component.
- No header is shown by default. A basic JS based header can be shown with
headerShown: trueoption (see header.md (./header.md)), or a native header can be shown by nesting a native stack navigator inside each tab screen.
Canonical Docs
Related Skills
- bottom-tabs.md (./bottom-tabs.md)
- stacks.md (./stacks.md)
- safe-areas.md (./safe-areas.md)
- header.md (./header.md)
references/safe-areas.md
title: Safe Areas
impact: HIGH
tags: react-navigation, safe-area, inset, safe-area-context, scroll-view, flatlist, edge-to-edge
Skill: Safe Areas
Description
Use this reference when content or scroll containers must avoid notches, system bars, home indicators, and other system UI.
React Navigation already handles safe areas for its built-in UI such as headers, tab bars, and drawers. Apply insets manually only for your own screen content or when you replace the built-in navigation UI with custom components.
When to Use
- Content is hidden behind the status bar, navigation bar, notch, or home indicator
- A screen uses a custom header, custom tab bar, or custom drawer content
- Fixed UI such as hero media, floating actions, or bottom actions needs selective safe-area padding
Edge-to-Edge on Android
Edge-to-edge makes the app content extend behind translucent system bars on Android, similar to how it works on iOS. Enable edge-to-edge for consistent safe area handling across platforms.
Android enables edge-to-edge by default from Android 15 (API level 35) for apps targeting API level 35. Edge-to-edge cannot be disabled from Android 16 (API level 36). For older Android versions, edge-to-edge can be enabled from React Native 0.81 onwards in android/gradle.properties:
edgeToEdgeEnabled=trueUsing contentInsetAdjustmentBehavior for ScrollView
When the main screen content scrolls (such as a ScrollView, FlatList, SectionList etc.) on iOS, prefer contentInsetAdjustmentBehavior="automatic" so the scroll view adjusts for safe areas while still allowing edge-to-edge scrolling behavior.
import { ScrollView } from 'react-native';
function ArticleScreen() {
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
{/* content */}
</ScrollView>
);
}On Android, conditionally use the useSafeAreaInsets hook from react-native-safe-area-context to apply paddings.
Using useSafeAreaInsets
Use the useSafeAreaInsets hook from react-native-safe-area-context for custom layouts. It gives precise control over which edges should receive padding.
import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function ScreenContent() {
const insets = useSafeAreaInsets();
return (
<View
style={{
flex: 1,
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
{/* content */}
</View>
);
}- Prefer the hook over
SafeAreaView, asSafeAreaViewcan behave poorly during animations, and mixingSafeAreaViewwith the hook can cause flicker. - It is usually not necessary to wrap the app in
SafeAreaProvideras it's done internally by React Navigation. Only add it when using in components not in stack, bottom tabs or drawer navigators. - Apply insets to only the specific edges that extend to the screen edge
- Keep landscape orientation in mind, as the top and bottom insets become left and right insets
- Do not wrap the whole app in a
SafeAreaViewto avoid wasting space.
Canonical Docs
- React Navigation: Supporting safe areas
- React Native: ScrollView
- React Navigation: Native Stack Navigator
- React Native 0.81: Android 16 support and edge-to-edge
- react-native-safe-area-context
Related Skills
- stacks.md (./stacks.md)
- bottom-tabs.md (./bottom-tabs.md)
- native-bottom-tabs.md (./native-bottom-tabs.md)
- drawers.md (./drawers.md)
references/stacks.md
title: Native Stack Navigator
impact: HIGH
tags: react-navigation, native-stack, navigation, header, header-items, search-bar, large-title, modal, animation, form-sheet
Skill: Native Stack Navigator
Description
Use createNativeStackNavigator for screen-to-screen flows.
When to Use
- Building the default push-based flow for an app
- Using platform-native headers, large titles, or a native search bar
- Presenting modal or sheet screens with native-stack presentations
Basic Example
Static API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}Common Features
Large Title
Use headerLargeTitleEnabled: true for an iOS large title that collapses into the regular header on scroll.
Static API
const AppStack = createNativeStackNavigator({
screens: {
Library: {
screen: LibraryScreen,
options: {
headerLargeTitleEnabled: true,
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="Library"
component={LibraryScreen}
options={{
headerLargeTitleEnabled: true,
}}
/>
</Stack.Navigator>
);
}- Only supported on iOS.
- The scroll view in the screen must use
contentInsetAdjustmentBehavior="automatic". - Don't set a background color on the header if large title is enabled as it makes title invisible on iOS 26.
- Don't set
headerTransparent: falseif large title is enabled. - If the scrollable area does not fill the screen, the large title will not collapse on scroll.
Header Search Bar
Use headerSearchBarOptions to render a native search bar. If the search configuration depends on screen state, update it with navigation.setOptions(...).
Static API
const AppStack = createNativeStackNavigator({
screens: {
Search: {
screen: SearchScreen,
options: {
headerSearchBarOptions: {
placeholder: 'Search',
},
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="Search"
component={SearchScreen}
options={{
headerSearchBarOptions: {
placeholder: 'Search',
},
}}
/>
</Stack.Navigator>
);
}- The scroll view in the screen must use
contentInsetAdjustmentBehavior="automatic". - If the screen doesn't have a scroll view, use
headerTopInsetEnabled: true.
Header Buttons and Custom Content
Use unstable_headerLeftItems and unstable_headerRightItems for native iOS header buttons or menus. Use headerLeft, headerRight, headerTitle, and headerBackground for custom React content, and as a fallback on other platforms.
Static API
import { Button, Text, View } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Profile: {
screen: ProfileScreen,
options: {
headerTitle: ({ tintColor, children }) => (
<Text style={{ color: tintColor, fontSize: 18 }}>{children}</Text>
),
headerBackground: () => (
<View style={{ flex: 1, backgroundColor: '#fff' }} />
),
unstable_headerLeftItems: () => [
{
type: 'button',
label: 'Edit',
onPress: () => {
// Do something
},
},
],
unstable_headerRightItems: () => [
{
type: 'button',
label: 'Done',
icon: {
type: 'sfSymbol',
name: 'checkmark',
},
onPress: () => {
// Do something
},
},
],
headerLeft: ({ tintColor }) => (
<Button title="Edit" color={tintColor} onPress={() => {}} />
),
headerRight: ({ tintColor }) => (
<Button title="Done" color={tintColor} onPress={() => {}} />
),
},
},
},
});Dynamic API
import { Button, Text, View } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{
headerTitle: ({ tintColor, children }) => (
<Text style={{ color: tintColor, fontSize: 18 }}>{children}</Text>
),
headerBackground: () => (
<View style={{ flex: 1, backgroundColor: '#fff' }} />
),
unstable_headerLeftItems: () => [
{
type: 'button',
label: 'Edit',
onPress: () => {
// Do something
},
},
],
unstable_headerRightItems: () => [
{
type: 'button',
label: 'Done',
icon: {
type: 'sfSymbol',
name: 'checkmark',
},
onPress: () => {
// Do something
},
},
],
headerLeft: ({ tintColor }) => (
<Button title="Edit" color={tintColor} onPress={() => {}} />
),
headerRight: ({ tintColor }) => (
<Button title="Done" color={tintColor} onPress={() => {}} />
),
}}
/>
</Stack.Navigator>
);
}unstable_headerLeftItemsandunstable_headerRightItemsare only supported on iOS and overrideheaderLeftandheaderRightwhen both are specified.- Use
headerLeftwhen replacing the back button. AddheaderBackVisible: trueif the back button should still be shown alongside the custom left element. headerTitleis useful when the title needs custom typography or extra layout, but custom title elements do not animate with the native title transition.- Use
headerBackgroundfor a gradient, image, or custom background view. For translucent native headers on iOS, preferheaderTransparent: truewithscrollEdgeEffectsfor iOS 26+ orheaderBlurEffectfor earlier versions. - Some behavior differs between iOS versions. On iOS 26+,
unstable_headerRightItemscan collapse into the system overflow menu when there is not enough space. - Custom items with
type: 'custom'inunstable_headerRightItemsare not collapsed into the overflow menu. - Labels are used when items collapse into the overflow menu, and for accessibility.
Screen Presentations
Use the screen presentation option to control whether a screen is pushed normally or shown as a modal or sheet.
Static API
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Compose: {
screen: ComposeScreen,
options: {
presentation: 'modal',
animation: 'slide_from_bottom',
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Compose"
component={ComposeScreen}
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
</Stack.Navigator>
);
}- Use
cardfor the default push presentation. - Use
modalfor a standard modal presentation. - Use
containedModalfor a current-context modal presentation on iOS and the default modal presentation on Android. - Use
fullScreenModalwhen the screen should take over the whole screen. On iOS, this presentation cannot be dismissed by gesture. - Use
transparentModalorcontainedTransparentModalwhen the previous screen should remain visible behind translucent content. - Use
formSheetwhen the design calls for a native sheet. Use form-sheet.md (./form-sheet.md) for detents and platform-specific caveats. - Use the
animationoption in the section below when the default transition does not fit the flow.
Transition Animations
Use animation option to customize the transition animation.
Static API
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Details: {
screen: DetailsScreen,
options: {
animation: 'fade_from_bottom',
animationDuration: 300,
},
},
},
});Dynamic API
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function AppStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Details"
component={DetailsScreen}
options={{
animation: 'fade_from_bottom',
animationDuration: 300,
}}
/>
</Stack.Navigator>
);
}- Supported
animationvalues includedefault,fade,fade_from_bottom,simple_push,slide_from_bottom,slide_from_right,slide_from_left,flip, andnone. fliprequirespresentation: 'modal'on iOS.slide_from_rightandslide_from_leftfall back to the default transition on iOS.simple_pushremoves the shadow and native header transition on iOS and falls back to the default transition on Android.- Use
animationTypeForReplace: 'pop'whennavigation.replace(...)should feel like going back, such as auth or onboarding flows. - Use
animationDurationon iOS to tuneslide_from_bottom,fade_from_bottom,fade, andsimple_push. It does not apply todefault,flip, or screens presented asmodalorformSheet. - Gesture-related transition options are only supported on iOS.
- Use
gestureEnabledto disable swipe-to-dismiss when the animation should only run programmatically. - Use
fullScreenGestureEnabledto start the dismiss gesture anywhere on the screen. This usessimple_push-style behavior, and the default iOS transition cannot be matched due to platform limitations. - Use
animationMatchesGesturewhen the interactive dismiss gesture should follow theanimationoption. It does not affect screens presented modally. - Use
fullScreenGestureShadowEnabledto control the shadow shown during a full-screen dismiss gesture. - On iOS,
gestureDirection: 'vertical'impliesanimation: 'slide_from_bottom'together with full-screen dismissal gestures.
Notes
- For scrollable content and
contentInsetAdjustmentBehavior="automatic", native stack needs it to be in the first-descendant chain — no views should be rendered before the scrollable view.
Canonical Docs
Related Skills
- form-sheet.md (./form-sheet.md)
- safe-areas.md (./safe-areas.md)
Frontmatter written into each target's SKILL.md.
Common
No fields set for this target.