Stack Navigation
Push/pop, headers, and screen options in app/ directories. Stack from expo-router is the file-based face of @react-navigation/native-stack - every sibling route file under a folder with _layout.tsx becomes a stack screen.
Search across all documentation pages
Push/pop, headers, and screen options in app/ directories. Stack from expo-router is the file-based face of @react-navigation/native-stack - every sibling route file under a folder with _layout.tsx becomes a stack screen.
Quick-reference recipe card - copy-paste ready.
npx expo install expo-routerapp/
├── _layout.tsx # Root stack
├── index.tsx
├── settings.tsx
└── orders/
├── _layout.tsx # Nested stack for /orders/*
├── index.tsx
└── [id].tsx// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack
screenOptions={{
headerShown: true,
animation: "slide_from_right",
}}
>
<Stack.Screen name="index" options={{ title: "Home" }} />
<Stack.Screen name="settings" options={{ title: "Settings" }} />
</Stack>
);
}// app/orders/_layout.tsx
import { Stack } from "expo-router";
export default function OrdersLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Orders" }} />
<Stack.Screen name="[id]" options={{ title: "Order" }} />
</Stack>
);
}When to reach for this:
presentation: "modal"headerShown: falseA root stack with a hidden-header splash, a settings screen with a dynamic title, and a nested orders stack.
// app/_layout.tsx
import { Stack } from "expo-router";
import { ThemeProvider } from "@/shared/theme";
export default function RootLayout() {
return (
<ThemeProvider>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="settings"
options={{
title: "Settings",
presentation: "card",
}}
/>
</Stack>
</ThemeProvider>
);
}// app/settings.tsx
import { useNavigation } from "expo-router";
import { useEffect } from "react";
import { ActivityIndicator, Text, View } from "react-native";
export default function SettingsScreen() {
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({ title: "Account Settings" });
}, [navigation]);
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>Settings content</Text>
</View>
);
}// app/orders/[id].tsx
import { useLocalSearchParams, Stack } from "expo-router";
import { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
export default function OrderDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const [title, setTitle] = useState(`Order #${id}`);
useEffect(() => {
// Simulate fetch - then update header via layout child
const timer = setTimeout(() => setTitle(`Order ${id} - Shipped`), 400);
return () => clearTimeout(timer);
}, [id]);
return (
<>
<Stack.Screen options={{ title }} />
<View style={{ flex: 1, padding: 16 }}>
<Text>Detail for order {id}</Text>
</View>
</>
);
}// Navigate between stack screens
import { Link, router } from "expo-router";
import { Pressable, Text, View } from "react-native";
export function HomeActions() {
return (
<View style={{ gap: 12 }}>
<Link href="/settings">Settings (declarative)</Link>
<Pressable onPress={() => router.push("/orders/42")}>
<Text>Order 42 (imperative)</Text>
</Pressable>
</View>
);
}app/orders/_layout.tsx → <Stack> wraps this folder
app/orders/index.tsx → initial route (bottom of stack)
app/orders/[id].tsx → pushed when navigating to /orders/:id
app/orders/new.tsx → pushed when navigating to /orders/newStack.Screen - explicit <Stack.Screen name="..."> is optional but recommended for options[id] directly_layout.tsx inherit the parent navigator| Layer | API | Precedence |
|---|---|---|
| Navigator default | screenOptions on <Stack> | Lowest |
| Layout child | <Stack.Screen options={...}> | Medium |
| Route file | export const options = {...} | Medium |
| Runtime | navigation.setOptions() or <Stack.Screen options> in render | Highest |
// Per-route static options (co-located)
export const options = {
title: "Profile",
headerBackTitle: "Back",
};<Stack.Screen
name="compose"
options={{
presentation: "modal", // iOS card modal; Android full-screen
gestureEnabled: true,
headerShown: true,
}}
/>card (default) - standard push animationmodal - modal presentation; pair with a (modals) route group for organizationtransparentModal - overlays previous screen (use sparingly - accessibility and Android back differ)fullScreenModal - edge-to-edge modal on iOS(tabs)/_layout.tsx → Tabs (tab bar always visible)
(tabs)/orders/_layout → Stack (push within Orders tab only)Each tab maintains an independent stack history - switching tabs preserves each tab's stack state by default. Reset stacks on logout with router.replace or remounting the navigator key.
Missing _layout.tsx in a folder with multiple screens - siblings may flatten into the parent stack unexpectedly. Fix: Add orders/_layout.tsx with <Stack /> when a folder has index + [id].
Duplicate screen names from index files - orders.tsx and orders/index.tsx conflict. Fix: Pick one URL shape per feature.
headerShown: false on root stack but expecting nested headers - child stacks must set headerShown: true explicitly. Fix: Configure headers at the nested _layout.tsx where screens live.
Dynamic title flicker - default title shows before fetch completes. Fix: Use a skeleton title or headerLargeTitle: false until data loads; set options in useEffect.
router.push after login duplicates auth on back stack - users swipe back to login. Fix: router.replace('/(tabs)') after successful auth.
Modal without Android back handling - presentation: "modal" still respects hardware back, but custom overlays may not. Fix: Test Android back; prefer real routes over absolute-position overlays.
Deep link opens stack mid-tree without index - /orders/99 may skip list screen. Fix: Acceptable for notifications; use router.back() fallback UI when stack is empty.
| Alternative | Use When | Don't Use When |
|---|---|---|
Stack (native-stack) | Default push/pop, native headers, gestures | You need cross-stack shared element transitions (limited) |
Tabs | Top-level sections with persistent tab bar | Linear wizard with no section switching |
Drawer | Many sections, tablet-first navigation | Phone apps with ≤4 primary destinations |
Modal as route (presentation: "modal") | Full-screen forms, compose flows | Tiny tooltips or dropdowns |
Imperative overlay (react-native Modal) | Blocking alerts, one-off pickers | Navigation that should appear in history/deep links |
React Navigation JS stack (@react-navigation/stack) | Custom card animations on older setups | New SDK 57 projects - native-stack is default |
// In _layout.tsx
<Stack.Screen name="login" options={{ headerShown: false }} />
// Or export from the route file
export const options = { headerShown: false };Set headerShown: false on the specific screen - parent screenOptions can still apply to siblings.
import { Stack, router } from "expo-router";
import { Pressable, Text } from "react-native";
export default function EditScreen() {
return (
<>
<Stack.Screen
options={{
headerRight: () => (
<Pressable onPress={() => router.push("/preview")}>
<Text>Preview</Text>
</Pressable>
),
}}
/>
{/* screen body */}
</>
);
}Use headerLeft, headerRight, or headerTitle render props - same API as React Navigation.
Yes - header: () => <MyHeader /> in screen options. Prefer design-system headers for consistency. Hide the default with headerShown: false and render your own in the screen body only when the stack header is insufficient.
Hardware back pops the current stack. At the root stack's first screen, back exits the app (or moves to background). Test nested stacks inside tabs - back should pop detail before switching tabs.
import { router } from "expo-router";
router.replace("/(auth)/login");replace removes prior routes from history. For complex resets, remount the navigator with a key tied to userId on the root layout.
Routes when the modal has a URL, deep link, or should appear in navigation history. RN Modal for ephemeral UI with no shareable path. Document team choice in an ADR when both patterns appear.
params: { id })See Typed Routes for param typing.
animation: "slide_from_right" | "fade" | "fade_from_bottom" | "flip" | "simple_push" | "none" in screenOptions. Platform defaults differ - verify on both iOS and Android simulators.
Yes - app/settings/_layout.tsx can export <Stack> while sitting inside the root stack. Limit depth to two stacks (root + feature) before UX and debugging suffer.
<Stack.Screen name="confirm-payment" options={{ gestureEnabled: false }} />Use for irreversible actions. Pair with an explicit cancel button for accessibility.
_layout.tsxapp/ - (modals) group patternStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026