AthenodeAthenode

Back to Mobile (Expo & React Native)

react-navigation

Created here

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.

SKILL.md

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-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.

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: 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

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

  • 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-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.

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: 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

  • 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 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

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

  • 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

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 or headerShown.

Canonical Docs

  • 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, 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

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>
  );
}
  • 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.

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>
  );
}
  • 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

  • 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-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

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, 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

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' 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

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>
  );
}
  • 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

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>
  );
}
  • 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

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, 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

  • 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=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.

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, 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

  • 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: false if large title is enabled.
  • If the scrollable area does not fill the screen, the large title will not collapse on scroll.

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_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

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 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

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 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

  • 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.

Ready to ship better, together?

Spec it. Decompose it. Ship it. All with your AI agent.

Start for free

Join engineers building with Athenode today.