This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

Add navigation

Edit page

In this chapter, learn how to add navigation to the Expo app.


In this chapter, we'll learn Expo Router's fundamentals to create stack navigation and a bottom tab bar with two tabs.

Watch: Adding navigation in your universal Expo app
Watch: Adding navigation in your universal Expo app

Set up file-based routing with Expo Router, create stack navigation between screens, and build a bottom tab bar.

Expo Router basics

Expo Router is a file-based routing framework for React Native and web apps. It manages navigation between screens and uses the same components across multiple platforms. To get started, we need to know about the following conventions:

  • app directory: A special directory containing only routes and their layouts. Any files added to this directory become a screen inside our native app and a page on the web. In the default template, it is located at src/app.
  • Root layout: The src/app/_layout.tsx file. It defines shared UI elements such as headers and tab bars so they are consistent between different routes.
  • File name conventions: Index file names, such as index.tsx, match their parent directory and do not add a path segment. For example, the index.tsx file in the src/app directory matches / route.
  • A route file exports a React component as its default value. It can use either .js, .jsx, .ts, or .tsx extension.
  • Android, iOS, and web share a unified navigation structure.

1

Add a new screen to the stack

Let's create a new file named about.tsx inside the src/app directory. It displays the screen name when the user navigates to the /about route.

src/app/about.tsx
import { Text, View, StyleSheet } from 'react-native'; export default function AboutScreen() { return ( <View style={styles.container}> <Text style={styles.text}>About screen</Text> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', justifyContent: 'center', alignItems: 'center', }, text: { color: '#fff', }, });

Inside src/app/_layout.tsx:

  1. Add a <Stack.Screen /> component and an options prop to update the title of the /about route.
  2. Update the /index route's title to Home by adding options prop.
src/app/_layout.tsx
import { Stack } from 'expo-router'; export default function RootLayout() { return ( <Stack> <Stack.Screen name="index" options={{ title: 'Home' }} /> <Stack.Screen name="about" options={{ title: 'About' }} /> </Stack> ); }
What is a Stack?

A stack navigator is the foundation for navigating between different screens in an app. On Android, a stacked route animates on top of the current screen. On iOS, a stacked route animates from the right. Expo Router provides a Stack component to create a navigation stack to add new routes.

2

We'll use Expo Router's Link component to navigate from the /index route to the /about route. It is a React component that renders a <Text> with a given href prop.

  1. Import the Link component from expo-router inside src/app/index.tsx.
  2. Add a Link component after <Text> component and pass href prop with the /about route.
  3. Add a style of fontSize, textDecorationLine, and color to Link component. It takes the same props as the <Text> component.
src/app/index.tsx
import { Text, View, StyleSheet } from 'react-native'; import { Link } from 'expo-router'; export default function Index() { return ( <View style={styles.container}> <Text style={styles.text}>Home screen</Text> <Link href="/about" style={styles.button}> Go to About screen </Link> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', justifyContent: 'center', }, text: { color: '#fff', }, button: { fontSize: 20, textDecorationLine: 'underline', color: '#fff', }, });

Let's take a look at the changes in our app. Click on Link to navigate to the /about route:

3

Add a not-found route

When a route doesn't exist, Expo Router displays a default screen for it. We can use a +not-found route to replace that screen with a custom fallback screen on mobile and web. Expo Router uses a special +not-found.tsx file to handle this case.

  1. Create a new file named +not-found.tsx inside the src/app directory to add the NotFoundScreen component.
  2. Add options prop from the Stack.Screen to display a custom screen title for this route.
  3. Add a Link component to navigate to the / route, which is our fallback route.
src/app/+not-found.tsx
import { View, StyleSheet } from 'react-native'; import { Link, Stack } from 'expo-router'; export default function NotFoundScreen() { return ( <> <Stack.Screen options={{ title: 'Oops! Not Found' }} /> <View style={styles.container}> <Link href="/" style={styles.button}> Go back to Home screen! </Link> </View> </> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', justifyContent: 'center', alignItems: 'center', }, button: { fontSize: 20, textDecorationLine: 'underline', color: '#fff', }, });

To test this, navigate to http://localhost:8081/123 URL in the web browser since it is easy to change the URL path there. The app should display the NotFoundScreen component:

4

Add a bottom tab navigator

At this point, the file structure of our src/app directory looks like the following:

src
 app
  _layout.tsxRoot layout
  index.tsxmatches route '/'
  about.tsxmatches route '/about'
  +not-found.tsxmatches route any 404 route

We'll add a bottom tab navigator to our app and reuse the existing Home and About screens to create a tab layout. This is a common navigation pattern in many social media apps, such as X and Bluesky. We'll also use the stack navigator in the Root layout so the +not-found route displays over any other nested navigators.

  1. Inside the src/app directory, add a (tabs) subdirectory. This special directory is used to group routes together and display them in a bottom tab bar.
  2. Create a (tabs)/_layout.tsx file inside the directory. It will be used to define the tab layout, which is separate from Root layout.
  3. Move the existing index.tsx and about.tsx files inside the (tabs) directory. The structure of src/app directory will look like this:
src
 app
  _layout.tsxRoot layout
  +not-found.tsxmatches route any 404 route
  (tabs)
   _layout.tsxTab layout
   index.tsxmatches route '/'
   about.tsxmatches route '/about'

Update the Root layout file to add a (tabs) route:

src/app/_layout.tsx
import { Stack } from 'expo-router'; export default function RootLayout() { return ( <Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> </Stack> ); }

Inside (tabs)/_layout.tsx, add a NativeTabs component to define the bottom tab layout. Native tabs use the platform's native tab bar on Android and iOS:

src/app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> </NativeTabs.Trigger> <NativeTabs.Trigger name="about"> <NativeTabs.Trigger.Label>About</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

Native tabs don't add a header to the screens, so the Home and About screens no longer have one. On web, the tabs appear as a list of labels at the top of the page.

Let's take a look at our app now to see the new bottom tabs:

5

Update bottom tab navigator appearance

Right now, the bottom tab navigator doesn't match the style of our app. For example, the tabs don't display icons, and the tab bar colors don't match the app's background color.

Modify the src/app/(tabs)/_layout.tsx file to add tab bar icons:

  1. Add a NativeTabs.Trigger.Icon component inside each NativeTabs.Trigger. The md prop takes a Material Symbols name for Android. The sf prop takes an SF Symbols name for iOS.
  2. Pass an object with default and selected keys to the sf prop. iOS displays the selected icon when the tab is active.
  3. Add the tintColor prop to the NativeTabs component and set its value to #ffd33d. This changes the color of the icon and label of the active tab.
src/app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/native-tabs'; export default function TabLayout() { return ( <NativeTabs tintColor="#ffd33d" > <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf={{ default: 'house', selected: 'house.fill' }} md="home" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="about"> <NativeTabs.Trigger.Label>About</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf={{ default: 'info.circle', selected: 'info.circle.fill' }} md="info" /> </NativeTabs.Trigger> </NativeTabs> ); }

The icons need no install step. On Android, the expo-symbols library from the default template provides Material Symbols. On iOS, SF Symbols are part of the system. On web, the tabs display only their labels.

Let's also change the background color of the tab bar using the backgroundColor prop:

src/app/(tabs)/_layout.tsx
<NativeTabs tintColor="#ffd33d" backgroundColor="#25292e">

The backgroundColor prop applies #25292e to the tab bar on Android and web. On iOS 26 and later, backgroundColor has no effect. The tab bar uses Liquid Glass, which follows the device's light or dark appearance.

Our app now has a custom bottom tabs navigator:

6

Update the ripple and indicator colors for Android

On Android, the tab bar still uses two colors from the default Material 3 theme. Pressing a tab displays a ripple effect. The active tab displays an indicator behind its icon. Neither color matches our app's color palette.

Add the indicatorColor and rippleColor props to the NativeTabs component in src/app/(tabs)/_layout.tsx:

src/app/(tabs)/_layout.tsx
<NativeTabs tintColor="#ffd33d" backgroundColor="#25292e" indicatorColor="rgba(255, 211, 61, 0.2)" rippleColor="rgba(255, 211, 61, 0.3)" >

In the above code:

  • indicatorColor sets the color of the indicator behind the active tab on Android, and the background of the active tab on web.
  • rippleColor sets the color of the ripple effect that displays when a user presses a tab on Android.

Now, the ripple and the indicator use the yellow color of the active tab:

Summary

Chapter 2: Add navigation

We've successfully added a stack and a tab navigator to our app.

In the next chapter, we'll learn how to build the app's first screen.

Next: Chapter 3: Build a screen