Expo UI and Router for iOSExpo SDK 57 and later, iOS only
Expo Router

Layouts and routes

Learn how to construct different relationships between pages by using directories and layout files.

Introduction to Expo Router Layout Files — What are layout files, how to navigate between screens, and block access using redirects.

Each directory within the src/app directory (including src/app itself) can define a layout in the form of a _layout.tsx file inside that directory.

  • This file defines how all the pages within that directory are arranged.
  • This is where you would define a stack navigator, tab navigator, drawer navigator, or any other layout that you want to use for the pages in that directory.
  • The layout file exports a default component that is rendered before whatever page you are navigating to within that directory.

Let's look at a few common layout scenarios.

Root layout

Virtually every app will have a _layout.tsx file directly inside the src/app directory.

  • This is the root layout and represents the entry point for your navigation.
  • In addition to describing the top-level navigator for your app, this file is where you would put initialization code that may have previously gone inside an App.jsx file, such as loading fonts, interacting with the splash screen, or adding context providers.

Here's an example root layout:

import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';

SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [loaded] = useFonts({
    SpaceMono: require('@/assets/fonts/SpaceMono-Regular.ttf'),
  });

  useEffect(() => {
    if (loaded) {
      SplashScreen.hide();
    }
  }, [loaded]);

  if (!loaded) {
    return null;
  }

  return <Stack />;
}

The above example shows the splash screen initially and then renders a stack navigator once the fonts are loaded, which will cause your app to proceed to the initial route.

Stacks

You can implement a stack navigator in your root layout, as shown above, or in any other layout file inside a directory. Let's suppose you have a file structure with a stack inside of a directory:

  • src
    • app
      • products
        • _layout.tsx
        • index.tsx
        • [productId].tsx
        • accessories
          • index.tsx

If you want everything inside the src/app/products directory to be arranged in a stack relationship, inside the _layout.tsx file, return a Stack component:

import { Stack } from 'expo-router';

export default function StackLayout() {
  return <Stack />;
}

When you navigate to /products, it will first go to the default route, which is products/index.tsx.

  • If you navigate to /products/123, then that page will be pushed onto the stack.
  • By default, the stack will render a back button in the header that will pop the current page off the stack, returning the user to the previous page.
  • Even when a page isn't visible, if it is still pushed onto the stack, it is still being rendered.

The Stack component implements React Navigation's native stack and can use the same screen options.

  • However, you do not have to define the pages specifically inside the navigator.
  • The files inside the directory will be automatically treated as eligible routes in the stack.
  • However, if you want to define screen options, you can add a Stack.Screen component inside the Stack component.
  • The name prop should match the route name, but you do not need to supply a component prop; Expo Router will map this automatically:
import { Stack } from 'expo-router';

export default function StackLayout() {
  return (
    <Stack>
      <Stack.Screen name="[productId]" options={{ headerShown: false }} />
    </Stack>
  );
}

While it is possible to nest navigators, be sure to only do so when it is truly needed.

  • In the above example, if you want to push products/accessories/index.tsx onto the stack, it's not necessary to have an additional _layout.tsx in the accessories directory with a Stack navigator.
  • That would define another stack inside the first one.
  • It is fine to add directories that only affect the URL, otherwise, use the same navigator as the parent directory.

Tabs

Expo Router provides multiple ways to implement tab navigation depending on your needs.

JavaScript tabs

You can implement a JavaScript-based tab navigator in a layout file using the Tabs component. All the routes directly inside that directory will be treated as tabs. Consider the following file structure:

  • src
    • app
      • (tabs)
        • _layout.tsx
        • index.tsx
        • feed.tsx
        • profile.tsx

In the _layout.tsx file, return a Tabs component:

import { Tabs } from 'expo-router';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => <MaterialIcons size={28} name="house.fill" color={color} />,
        }}
      />
      <Tabs.Screen name="feed" options={{ title: 'Feed' }} />
      <Tabs.Screen name="profile" options={{ title: 'Profile' }} />
    </Tabs>
  );
}

This will cause the index.tsx, feed.tsx, and profile.tsx files to appear together in the same bottom tabs navigator. This Tabs component uses React Navigation's native bottom tabs and supports the same options.

In the case of Tabs, you will likely want to define the tabs in the navigator, as this influences the order in which tabs appear, the title, and the icon inside the tab. The index route will be the default selected tab.

Native tabs

On Android and iOS, you can use native tabs to render the platform's built-in tab bar. Native tabs provide expected platform behaviors like scroll-to-top on tap, native animations, and a native look and feel.

Like JavaScript tabs, native tabs can be used in a layout file inside a route group directory:

  • src
    • app
      • (tabs)
        • _layout.tsx
        • index.tsx
        • feed.tsx
        • profile.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/home.png')} />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="feed">
        <NativeTabs.Trigger.Label>Feed</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/feed.png')} />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="profile">
        <NativeTabs.Trigger.Label>Profile</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/profile.png')} />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Platform-specific tabs

Since native tabs are only available on Android and iOS, a common pattern is to use platform-specific file extensions to provide different tab implementations for native and web. The root layout renders a tab component, and Expo's module resolution automatically picks the correct file based on the platform.

  • src
    • app
      • _layout.tsx
      • index.tsx
      • explore.tsx
    • components
      • app-tabs.native.tsxNative tabs (Android and iOS)
      • app-tabs.tsxCustom tabs (web)

The root layout imports and renders the AppTabs component. app-tabs.native.tsx is used on Android and iOS, and app-tabs.tsx on web:

import AppTabs from '@/components/app-tabs';

export default function RootLayout() {
  return <AppTabs />;
}

On Android and iOS, app-tabs.native.tsx uses native tabs:

import { NativeTabs } from 'expo-router/native-tabs';

export default function AppTabs() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/home.png')} />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="explore">
        <NativeTabs.Trigger.Label>Explore</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon src={require('@/assets/images/tabIcons/explore.png')} />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui';

export default function AppTabs() {
  return (
    <Tabs>
      <TabSlot />
      <TabList>
        <TabTrigger name="index" href="/">
          Home
        </TabTrigger>
        <TabTrigger name="explore" href="/explore">
          Explore
        </TabTrigger>
      </TabList>
    </Tabs>
  );
}

Slot

In some cases, you may want a layout without a navigator.

  • This is helpful for adding a header or footer around the current route, or for displaying a modal over any route inside a directory.
  • In this case, you can use the Slot component, which serves as a placeholder for the current child route.

Consider the following file structure:

  • src
    • app
      • social
        • _layout.tsx
        • index.tsx
        • feed.tsx
        • profile.tsx

For example, you may want to wrap any route inside the social directory with a header and footer, but you want navigating between the pages to simply replace the current page rather than pushing new pages onto a stack, which can then later be popped off with a "back" navigation action. In the _layout.tsx file, return a Slot component surrounded by your header and footer:

import { Slot } from 'expo-router';

export default function Layout() {
  return (
    <>
      <Header />
      <Slot />
      <Footer />
    </>
  );
}

Other layouts

These are just a few examples of common layouts to give you an idea of how it works. There's much more you can do with layout:

Common patterns

Now that you know the basics of how files and directories are named and arranged in Expo Router, let's apply that knowledge, looking at some real-life navigation patterns you might use in your app.

Stacks inside tabs: nested navigators

If the typical starting point for your app is a set of tabs, but one or more tabs may have more than one screen associated with it, nesting a stack navigator inside of a tab is often the way to go. This pattern often results in intuitive URLs and scales well to desktop web apps, where the primary tabs are often always visible.

Consider the following navigation tree:

  • src
    • app
      • (tabs)
        • _layout.tsx
        • index.tsxsingle page tab
        • feed
          • _layout.tsxtab with a stack inside
          • index.tsx
          • [postId].tsx
        • settings.tsxsingle page tab

In the src/app/(tabs)/_layout.tsx file, return a Tabs component:

import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ headerShown: false }}>
      <Tabs.Screen name="index" options={{ title: 'Home' }} />
      <Tabs.Screen name="feed" options={{ title: 'Feed' }} />
      <Tabs.Screen name="settings" options={{ title: 'Settings' }} />
    </Tabs>
  );
}

In the src/app/(tabs)/feed/_layout.tsx file, return a Stack component:

import { Stack } from 'expo-router';

export const unstable_settings = {
  initialRouteName: 'index',
};

export default function FeedLayout() {
  return <Stack />;
}

Now, within the src/app/(tabs)/feed directory, you can have Link components that point to different posts (for example, /feed/123). Those links will push the feed/[postId] route onto the stack, leaving the tab navigator visible.

You can also navigate from any other tab to a post in the feed tab with the same URL. Use withAnchor in conjunction with initialRouteName to ensure that the feed/index route is always the first screen in the stack:

<Link href="/feed/123" withAnchor>
  Go to post
</Link>

You can also nest tabs inside of an outer stack navigator. That is often more useful for displaying modals over the tabs.

Going back to the tab you came from

A Link to a URL that belongs to another tab switches tabs, and the back button then pops the stack inside that tab.

  • In the example above, opening /feed/123 from the Settings tab lands on the post in the Feed tab.
  • Going back returns to feed/index, not to Settings.

With native tabs, the tab navigator loads every tab as soon as it mounts.

  • A tab that contains a stack is already sitting at that stack's first screen before you link into it.
  • That screen ends up underneath the one you linked to whether or not you pass withAnchor.

If you want the back button to return to the screen you came from, keep the detail route out of the tabs and push it from an outer stack instead:

  • src
    • app
      • _layout.tsxstack that contains the tabs
      • (tabs)
        • _layout.tsx
        • index.tsx
        • feed
          • _layout.tsx
          • index.tsx
        • settings.tsx
      • feed
        • [postId].tsxpushed above the tab bar
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="feed/[postId]" options={{ title: 'Post' }} />
    </Stack>
  );
}

/feed/123 is now a screen of the root stack rather than of the Feed tab. The router pushes it on top of whichever tab is open, and going back returns to that tab. This is also how detail screens usually behave in iOS apps.

Nested navigators — Learn more about how to use nested navigators in your Expo Router app.

Different tabs per platform: platform-specific tabs

When building a cross-platform app, you may want to use native tabs on Android and iOS for a platform-native look and feel, while using custom tabs on web for full control over styling. You can achieve this using platform-specific file extensions.

  • src
    • app
      • _layout.tsximports AppTabs
      • index.tsx
      • feed.tsx
      • profile.tsx
    • components
      • app-tabs.native.tsxAppTabs (native tabs) for Android and iOS
      • app-tabs.tsxAppTabs (custom tabs) for web

The root layout renders an AppTabs component. Expo's module resolution automatically picks app-tabs.native.tsx on Android and iOS, and app-tabs.tsx on web, allowing each platform to use a tab implementation suited to its conventions.

For a complete example of this pattern with code, see Platform-specific tabs in the Layout guide.

One screen, two tabs: sharing routes

Route groups can be used to share a single screen between two different tabs. Consider a navigation tree that has a Feed tab and a Search tab, and they both share pages for viewing a user profile:

  • src
    • app
      • (tabs)
        • _layout.tsx
        • (feed)
          • index.tsxdefault route
        • (search)
          • search.tsx
        • (feed,search)
          • _layout.tsxlayout shared between the two tabs
          • users
            • [username].tsxshared user profile page

Each of the tabs is put in a group so you can define a third directory that shares routes between two groups (src/app/(tabs)/(feed,search)/). Even with the extra layer, src/app/(tabs)/(feed)/index.tsx is still the nearest index, so it will be the default route.

import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="(feed)" options={{ title: 'Feed' }} />
      <Tabs.Screen name="(search)" options={{ title: 'Search' }} />
    </Tabs>
  );
}

Both the (feed) and (search) route groups contain stacks, so they can also share a single layout:

import { Stack } from 'expo-router';

export default function SharedLayout() {
  return <Stack />;
}

It's also possible for shared groups to only contain the shared pages, with each distinct group having its own layout file.

Now, both tabs can navigate to /users/evanbacon and see the same user profile page.

When you're already focused on a tab and navigating to a user, you will stay in that current tab's group.

  • But when deep-linking directly to a user profile page from outside the app, Expo Router has to pick one of the two groups, so it will pick the first group alphabetically.
  • Therefore, deep-linking to /users/evanbacon will show the user profile in the Feed tab.

Shared routes — Learn more about how distinct routes can share the same URL in Expo Router.

Authenticated users only: protected routes

For mobile apps requiring authentication, you will likely have a set of routes that should only be accessible to authenticated users.

For example, consider the following navigation tree in which you have a bottom tabs layout, a sign-in page, a create account page, and a modal that should only be visible to authenticated users:

  • src
    • app
      • _layout.tsxRoot layout
      • (tabs)
        • _layout.tsx
        • index.tsxProtected
        • settings.tsxProtected
      • sign-in.tsx
      • create-account.tsx
      • modal.tsxProtected

When your app is first launched, the router will try to open the root index, src/app/(tabs)/index.tsx.

  • If you wrap this screen in a Stack.Protected with the guard={false}, the screen will become inaccessible and the next available screen will be opened instead.
  • In this example, the sign-in screen will be opened, since it is the next available route.
import { Stack } from 'expo-router';
import { useAuthState } from '@/utils/authState';

export default function RootLayout() {
  const { isLoggedIn } = useAuthState();

  return (
    <Stack>
      <Stack.Protected guard={isLoggedIn}>
        <Stack.Screen name="(tabs)" />
        <Stack.Screen name="modal" />
      </Stack.Protected>

      <Stack.Protected guard={!isLoggedIn}>
        <Stack.Screen name="sign-in" />
        <Stack.Screen name="create-account" />
      </Stack.Protected>
    </Stack>
  );
}

This way, you can fetch your auth state from a store and show the appropriate screens. If the auth state changes, the layout will re-render, so if isLoggedIn changes from false to true, the app will automatically navigate to the root of the (tabs) group.

Another benefit of protected routes is that they are checked even if you deep link into a page directly. For example, if an unauthenticated user deep links into the modal screen above, they will be redirected to the sign-in page.

Protected routes can also be used to conditionally show bottom tabs. In this example, the vip tab will only be shown to authenticated users who are VIP members:

import { Stack } from 'expo-router';
import { useAuthState } from '@/utils/authState';

export default function TabsLayout() {
  const { isVip } = useAuthState();

  return (
    <Tabs>
      <Tabs.Screen name="index" />

      <Tabs.Protected guard={isVip}>
        <Tabs.Screen name="vip" />
      </Tabs.Protected>

      <Tabs.Screen name="settings" />
    </Tabs>
  );
}

Expo Router authentication — Follow an in-depth guide for implementing authentication using protected routes.

Sometimes the best route isn't a route at all

Separating your navigation states into distinct routes is meant to serve you and your app.

  • Sometimes the best pattern for the job will not involve navigating to another route at all.
  • Since layout files are just React components, you can use them to display all sorts of UI around, besides, or instead of a navigator.

Thinking back to authentication, the protected route setup works great if the user should simply not be able to visit certain pages without logging in.

  • But what about when unauthenticated users can browse an app in read-only mode?
  • In that case, you might want to show a login modal over the app, rather than redirecting the user to a login page:
import { Modal } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Stack } from 'expo-router';

export default function Layout() {
  const isAuthenticated = /* check for valid auth token / session */

  return (
    <SafeAreaView>
      <Stack />
      <Modal visible={!isAuthenticated}>{/* login UX */}</Modal>
    </SafeAreaView>
  );
}

Modals in Expo Router — Learn multiple patterns for displaying modals in Expo Router, including using a modal inside of a layout file.

Nested navigators

Navigation UI elements (Link, Tabs, Stack) may move out of the Expo Router library in the future.

Using a Stack Navigator with Expo Router — Navigate between screens, pass params between screens, create dynamic routes, and configure the screen titles and animations.

Nesting navigators allow rendering a navigator inside the screen of another navigator.

Example

Consider the following file structure which is used as an example:

  • src
    • app
      • _layout.tsx
      • index.tsx
      • home
        • _layout.tsx
        • feed.tsx
        • messages.tsx

In the above example, src/app/home/feed.tsx matches /home/feed, and src/app/home/messages.tsx matches /home/messages.

import { Stack } from 'expo-router';

export default Stack;

Both src/app/home/_layout.tsx and src/app/index.tsx below are nested in the src/app/_layout.tsx layout so that it will be rendered as a stack.

import { Tabs } from 'expo-router';

export default Tabs;
import { Link } from 'expo-router';

export default function Root() {
  return <Link href="/home/messages">Navigate to nested route</Link>;
}

Both src/app/home/feed.tsx and src/app/home/messages.tsx below are nested in the home/_layout.tsx layout, so it will be rendered as a tab.

import { View, Text } from 'react-native';

export default function Feed() {
  return (
    <View>
      <Text>Feed screen</Text>
    </View>
  );
}
import { View, Text } from 'react-native';

export default function Messages() {
  return (
    <View>
      <Text>Messages screen</Text>
    </View>
  );
}

Stack inside native tabs

When using native tabs, you can nest a <Stack /> layout inside each tab to support headers and pushing screens. For a complete example, see Use Stacks inside tabs.

Navigate to a screen in a nested navigator

In React Navigation, navigating to a specific nested screen can be controlled by passing the screen name in params. This renders the specified nested screen instead of the initial screen for that nested navigator.

For example, from the initial screen inside the root navigator, you want to navigate to a screen called media inside settings (a nested navigator). In React Navigation, this is done as shown in the example below:

navigation.navigate('root', {
  screen: 'settings',
  params: {
    screen: 'media',
  },
});

In Expo Router, you can use router.push() to achieve the same result. There is no need to pass the screen name in the params explicitly.

router.push('/root/settings/media');

Shared routes

To match the same URL with different layouts, use groups with overlapping child routes.

  • This pattern is very common in native apps.
  • For example, in the X app, a profile can be viewed in every tab (such as home, search, and profile).
  • However, there is only one URL that is required to access this route.

In the example below, src/app/_layout.tsx is the tab bar and each route has its own header. The src/app/(profile)/[user].tsx route is shared between each tab.

  • src
    • app
      • _layout.tsx
      • (home)
        • _layout.tsx
        • [user].tsx
      • (search)
        • _layout.tsx
        • [user].tsx
      • (profile)
        • _layout.tsx
        • [user].tsx

Group segments are not part of the URL, so every shared route matches the same URL.

  • Expo Router uses the group you are in to select between them.
  • In-app navigation keeps the current group.
  • A page reload, a bookmark, a shared URL, and a deep link are all cold links.
  • A cold link has no current group, so Expo Router renders the first alphabetical match.
  • The same URL can therefore render one screen after in-app navigation and a different screen after a page reload.

Shared routes can be navigated directly by including the group name in the route. For example, /(search)/baconbrix navigates to /baconbrix in the "search" layout. Use this form when a link must always open one specific group.

Do not use shared routes to give different user roles a different version of a screen. The URL does not carry the user's role, so a cold link cannot select the correct group, and protected routes do not change that. Declare the screen once and control access with Stack.Protected.

Arrays

Array syntax is an advanced concept that is unique to native app development.

Instead of defining the same route multiple times with different layouts, use the array syntax (,) to duplicate the children of a group. For example, src/app/(home,search)/[user].tsx — creates src/app/(home)/[user].tsx and src/app/(search)/[user].tsx in memory.

To distinguish between the two routes use a layout's segment prop:

export default function DynamicLayout({ segment }) {
  if (segment === '(search)') {
    return <SearchStack />;
  }

  return <Stack />;
}

To enable the array syntax, specify the initialRouteName for each group using unstable_settings object in the dynamic layout:

export const unstable_settings = {
  initialRouteName: 'home',
  search: {
    initialRouteName: 'search',
  },
};

export default function DynamicLayout({ segment }) {
   ... 
}

In the above example, the home route is the default route for the home group and the app. The search route is the default route for the search group.

Key points

  • You can only provide groups for the current navigator.
  • When using the array syntax, if there are two groups (for example, (one)/(two)), only the last group's segment is used for matching the route.
  • If there are at least two group initialRouteNames, but a default initialRouteName is not provided, the first group's initialRouteName is used.

Sources

The original Expo docs for this page:

On this page