---
modificationDate: April 14, 2026
title: Common navigation patterns in Expo Router
description: Apply Expo Router basics to real-life navigation patterns you could use in your app.
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/router/basics/common-navigation-patterns/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# Common navigation patterns in Expo Router

Apply Expo Router basics to real-life navigation patterns you could use in your app.

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

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.tsx``single page tab`

   `feed`

    `_layout.tsx``tab with a stack inside`

    `index.tsx`

    `[postId].tsx`

   `settings.tsx``single page tab`

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

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

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

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

[Nested navigators](/router/advanced/nesting-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](/router/advanced/native-tabs) on Android and iOS for a platform-native look and feel, while using [custom tabs](/router/advanced/custom-tabs) on web for full control over styling. You can achieve this using [platform-specific file extensions](/router/advanced/platform-specific-modules).

`src`

 `app`

  `_layout.tsx``imports AppTabs`

  `index.tsx`

  `feed.tsx`

  `profile.tsx`

 `components`

  `app-tabs.native.tsx``AppTabs (native tabs) for Android and iOS`

  `app-tabs.tsx``AppTabs (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](/router/basics/navigation-layouts#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.tsx``default route`

   `(search)`

    `search.tsx`

   `(feed,search)`

    `_layout.tsx``layout shared between the two tabs`

    `users`

     `[username].tsx``shared 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.

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

```tsx
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](/router/advanced/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.tsx``Root layout`

  `(tabs)`

   `_layout.tsx`

   `index.tsx``Protected`

   `settings.tsx``Protected`

  `sign-in.tsx`

  `create-account.tsx`

  `modal.tsx``Protected`

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.

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

```tsx
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](/router/advanced/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:

```tsx
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](/router/advanced/modals) — Learn multiple patterns for displaying modals in Expo Router, including using a modal inside of a layout file.
