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**
```tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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-icons` if Expo SDK is configured in the app
- `react-native-vector-icons` for 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.
```tsx
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**
```tsx
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**
```tsx
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`.
```tsx
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**
```tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
tabBar: (props) => <MyTabBar {...props} />,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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**
```tsx
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**
```tsx
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: false` to 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**
```tsx
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**
```tsx
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
- [Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator)
- [Customizing bottom tab bar](https://reactnavigation.org/docs/customizing-tabbar)
- [Hiding tab bar in specific screens](https://reactnavigation.org/docs/hiding-tabbar-in-screens)
## 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`](https://reactnavigation.org/docs/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**
```tsx
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
```
**Dynamic API**
```tsx
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.
```tsx
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`](https://reactnavigation.org/docs/drawer-layout/) for the outer drawer and keep Drawer navigator for the inner navigation drawer.
```tsx
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**
```tsx
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**
```tsx
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-icons` if Expo SDK is configured in the app
- `react-native-vector-icons` for 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`.
```tsx
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**
```tsx
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
drawerContent: (props) => <CustomDrawerContent {...props} />,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
Help: HelpScreen,
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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**
```tsx
import { createDrawerNavigator } from '@react-navigation/drawer';
const AppDrawer = createDrawerNavigator({
screenOptions: {
drawerPosition: 'right',
},
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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**
```tsx
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**
```tsx
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: false` to 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
- [Drawer Navigator](https://reactnavigation.org/docs/drawer-navigator)
- [Multiple drawers](https://reactnavigation.org/docs/multiple-drawers)
- [Static configuration](https://reactnavigation.org/docs/static-configuration)
- [Configuring the header bar](https://reactnavigation.org/docs/headers)
## 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**
```tsx
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**
```tsx
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 `sheetInitialDetentIndex` to 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**
```tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Filters: {
screen: FiltersScreen,
options: {
presentation: 'formSheet',
sheetAllowedDetents: 'fitToContents',
},
},
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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
- `sheetGrabberVisible` is iOS only and shows the grabber.
- `sheetCornerRadius` overrides the default corner radius.
- `sheetElevation` is Android only, adjusts the top-edge shadow, and cannot be changed after mount.
- `sheetLargestUndimmedDetentIndex` controls 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.backgroundColor` sets 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**
```tsx
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**
```tsx
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**
```tsx
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**
```tsx
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 or `headerShown`.
## Canonical Docs
- [Native Stack Navigator](https://reactnavigation.org/docs/native-stack-navigator)
## 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**
```tsx
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**
```tsx
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`, and `headerTitleAlign` for the default header. Put shared config in navigator `screenOptions` and per-screen overrides in `options`.
- Use `headerShown: false` for 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**
```tsx
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**
```tsx
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>
);
}
```
- `headerLeft` and `headerRight` receive props such as `tintColor`, `pressColor`, and `pressOpacity`.
- `headerTitle` is useful when the title needs custom typography, icons, or extra layout.
- Use `headerTitleContainerStyle`, `headerLeftContainerStyle`, and `headerRightContainerStyle` to 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**
```tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const AppTabs = createBottomTabNavigator({
screens: {
Search: {
screen: SearchScreen,
options: {
headerSearchBarOptions: {
placeholder: 'Search',
onChangeText: (text) => {
// Do something
},
},
},
},
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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>
);
}
```
- `headerStyle` does not apply when `headerBackground` is used. Style the element returned from `headerBackground` instead.
- Transparent headers overlap the content below. Add top spacing manually using `useHeaderHeight` hook from `@react-navigation/elements`
## Canonical Docs
- [Configuring the header bar](https://reactnavigation.org/docs/headers)
- [Bottom Tabs Navigator](https://reactnavigation.org/docs/bottom-tab-navigator)
- [Drawer Navigator](https://reactnavigation.org/docs/drawer-navigator)
## 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**
```tsx
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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-icons` if Expo SDK is configured in the app
- `react-native-vector-icons` for 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**
```tsx
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**
```tsx
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**
```tsx
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**
```tsx
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, with `marginHorizontal: 'auto'` to center it under the tab button.
- Don't set `borderRadius` if `tabBarItemStyle` has `'auto'` width as it's not supported.
If the default styling options aren't sufficient, use `tabBarIndicator` to render a custom indicator.
**Static API**
```tsx
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**
```tsx
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`.
```tsx
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**
```tsx
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const HomeTabs = createMaterialTopTabNavigator({
tabBar: (props) => <MyTabBar {...props} />,
screens: {
Feed: FeedScreen,
Updates: UpdatesScreen,
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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
- [Material Top Tabs Navigator](https://reactnavigation.org/docs/material-top-tab-navigator)
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**
```tsx
import { createNativeBottomTabNavigator } from '@react-navigation/bottom-tabs/unstable';
const AppTabs = createNativeBottomTabNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
```
**Dynamic API**
```tsx
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.
```tsx
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**
```tsx
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**
```tsx
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'` and `name`.
- Local images with `type: 'image'` and `source`.
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`, and `3x` image assets (`image.png`, `image@2x.png`, `image@3x.png`) because iOS does not scale tab icons automatically.
- Set `tinted: false` on 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**
```tsx
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**
```tsx
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**
```tsx
const AppTabs = createNativeBottomTabNavigator({
screenOptions: {
bottomAccessory: ({ placement }) => {
return (
<View style={{ padding: 16 }}>
<Text>Placement: {placement}</Text>
</View>
);
},
},
screens: {
Home: HomeScreen,
},
});
```
**Dynamic API**
```tsx
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>
);
}
```
- `placement` is `regular` above the bar or `inline` when 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**
```tsx
const AppTabs = createNativeBottomTabNavigator({
screenOptions: {
tabBarControllerMode: 'tabSidebar',
},
screens: {
Home: HomeScreen,
Search: SearchScreen,
},
});
```
**Dynamic API**
```tsx
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>
);
}
```
- `auto` lets the system choose the presentation.
- `tabBar` forces 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**
```tsx
const AppTabs = createNativeBottomTabNavigator({
screenOptions: {
tabBarMinimizeBehavior: 'onScrollDown',
},
screens: {
Home: HomeScreen,
},
});
```
**Dynamic API**
```tsx
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`, and `onScrollUp`.
- 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-screens` along with the latest version of `@react-navigation/bottom-tabs` to 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: true` option (see [header.md](./header.md)), or a native header can be shown by nesting a native stack navigator inside each tab screen.
## Canonical Docs
- [Native Bottom Tabs Navigator](https://reactnavigation.org/docs/native-bottom-tab-navigator)
- [Native Stack Navigator](https://reactnavigation.org/docs/native-stack-navigator)
## 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`:
```ini
edgeToEdgeEnabled=true
```
## Using `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.
```tsx
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.
```tsx
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`, as `SafeAreaView` can behave poorly during animations, and mixing `SafeAreaView` with the hook can cause flicker.
- It is usually not necessary to wrap the app in `SafeAreaProvider` as 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 `SafeAreaView` to avoid wasting space.
## Canonical Docs
- [React Navigation: Supporting safe areas](https://reactnavigation.org/docs/handling-safe-area/)
- [React Native: ScrollView](https://reactnative.dev/docs/scrollview)
- [React Navigation: Native Stack Navigator](https://reactnavigation.org/docs/native-stack-navigator)
- [React Native 0.81: Android 16 support and edge-to-edge](https://reactnative.dev/blog/2025/08/12/react-native-0.81)
- [react-native-safe-area-context](https://github.com/AppAndFlow/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**
```tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
```
**Dynamic API**
```tsx
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**
```tsx
const AppStack = createNativeStackNavigator({
screens: {
Library: {
screen: LibraryScreen,
options: {
headerLargeTitleEnabled: true,
},
},
},
});
```
**Dynamic API**
```tsx
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: false` if 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**
```tsx
const AppStack = createNativeStackNavigator({
screens: {
Search: {
screen: SearchScreen,
options: {
headerSearchBarOptions: {
placeholder: 'Search',
},
},
},
},
});
```
**Dynamic API**
```tsx
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**
```tsx
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**
```tsx
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_headerLeftItems` and `unstable_headerRightItems` are only supported on iOS and override `headerLeft` and `headerRight` when both are specified.
- Use `headerLeft` when replacing the back button. Add `headerBackVisible: true` if the back button should still be shown alongside the custom left element.
- `headerTitle` is useful when the title needs custom typography or extra layout, but custom title elements do not animate with the native title transition.
- Use `headerBackground` for a gradient, image, or custom background view. For translucent native headers on iOS, prefer `headerTransparent: true` with `scrollEdgeEffects` for iOS 26+ or `headerBlurEffect` for earlier versions.
- Some behavior differs between iOS versions. On iOS 26+, `unstable_headerRightItems` can collapse into the system overflow menu when there is not enough space.
- Custom items with `type: 'custom'` in `unstable_headerRightItems` are 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**
```tsx
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Compose: {
screen: ComposeScreen,
options: {
presentation: 'modal',
animation: 'slide_from_bottom',
},
},
},
});
```
**Dynamic API**
```tsx
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 `card` for the default push presentation.
- Use `modal` for a standard modal presentation.
- Use `containedModal` for a current-context modal presentation on iOS and the default modal presentation on Android.
- Use `fullScreenModal` when the screen should take over the whole screen. On iOS, this presentation cannot be dismissed by gesture.
- Use `transparentModal` or `containedTransparentModal` when the previous screen should remain visible behind translucent content.
- Use `formSheet` when the design calls for a native sheet. Use [form-sheet.md](./form-sheet.md) for detents and platform-specific caveats.
- Use the `animation` option 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**
```tsx
const AppStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Details: {
screen: DetailsScreen,
options: {
animation: 'fade_from_bottom',
animationDuration: 300,
},
},
},
});
```
**Dynamic API**
```tsx
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 `animation` values include `default`, `fade`, `fade_from_bottom`, `simple_push`, `slide_from_bottom`, `slide_from_right`, `slide_from_left`, `flip`, and `none`.
- `flip` requires `presentation: 'modal'` on iOS.
- `slide_from_right` and `slide_from_left` fall back to the default transition on iOS.
- `simple_push` removes the shadow and native header transition on iOS and falls back to the default transition on Android.
- Use `animationTypeForReplace: 'pop'` when `navigation.replace(...)` should feel like going back, such as auth or onboarding flows.
- Use `animationDuration` on iOS to tune `slide_from_bottom`, `fade_from_bottom`, `fade`, and `simple_push`. It does not apply to `default`, `flip`, or screens presented as `modal` or `formSheet`.
- Gesture-related transition options are only supported on iOS.
- Use `gestureEnabled` to disable swipe-to-dismiss when the animation should only run programmatically.
- Use `fullScreenGestureEnabled` to start the dismiss gesture anywhere on the screen. This uses `simple_push`-style behavior, and the default iOS transition cannot be matched due to platform limitations.
- Use `animationMatchesGesture` when the interactive dismiss gesture should follow the `animation` option. It does not affect screens presented modally.
- Use `fullScreenGestureShadowEnabled` to control the shadow shown during a full-screen dismiss gesture.
- On iOS, `gestureDirection: 'vertical'` implies `animation: '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
- [Native Stack Navigator](https://reactnavigation.org/docs/native-stack-navigator)
- [Moving between screens](https://reactnavigation.org/docs/navigating)
- [Configuring the header bar](https://reactnavigation.org/docs/headers)
- [Opening a modal](https://reactnavigation.org/docs/modal)
## Related Skills
- [form-sheet.md](./form-sheet.md)
- [safe-areas.md](./safe-areas.md)
SKILL.md
---
name: react-navigation
description: 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.
license: MIT
---
# 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][stacks] | Stack based navigation |
| [form-sheet.md][form-sheet] | Bottom sheet and form sheets |
| [bottom-tabs.md][bottom-tabs] | Cross-platform bottom tabs |
| [native-bottom-tabs.md][native-bottom-tabs] | Native bottom tabs |
| [material-top-tabs.md][material-top-tabs] | Swipeable Top tabs |
| [drawers.md][drawers] | Drawer navigation and sidebars |
| [header.md][header] | Configuring headers |
| [safe-areas.md][safe-areas] | Safe-area handling |
## Problem -> Skill Mapping
| Problem | Start With |
| ------------------------------------------------------------------------- | ------------------------------------------- |
| Showing screens and modals in a stack | [stacks.md][stacks] |
| Showing bottom sheets or form sheets | [form-sheet.md][form-sheet] |
| Showing screens in bottom tabs or responsive sidebars with web support | [bottom-tabs.md][bottom-tabs] |
| Showing screens in native tabs on iOS & Android | [native-bottom-tabs.md][native-bottom-tabs] |
| Showing content in swipeable top tabs | [material-top-tabs.md][material-top-tabs] |
| Using a drawer or sidebar | [drawers.md][drawers] |
| Configuring the header in bottom tab or drawer navigator | [header.md][header] |
| Handling safe-area such as status bar, header insets, tab bar insets etc. | [safe-areas.md][safe-areas] |
[stacks]: references/stacks.md
[form-sheet]: references/form-sheet.md
[safe-areas]: references/safe-areas.md
[bottom-tabs]: references/bottom-tabs.md
[native-bottom-tabs]: references/native-bottom-tabs.md
[material-top-tabs]: references/material-top-tabs.md
[drawers]: references/drawers.md
[header]: references/header.md