Stack
Edit this page
Learn how to use the Stack navigator in Expo Router.

Navigate between screens, pass params between screens, create dynamic routes, and configure the screen titles and animations.
A stack navigator is the foundational way of navigating between routes 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
navigation component that creates a navigation stack and allows you to add new routes in your app.
This guide provides information on how you can create a Stack
navigator in your project and customize an individual route's options and header.
Get started
You can use file-based routing to create a stack navigator. Here's an example file structure:
app
_layout.tsx
index.tsx
details.tsx
This file structure produces a layout where the index
route is the first route in the stack, and the details
route is pushed on top of the index
route when navigated.
You can use the app/_layout.tsx file to define your app's Stack
navigator with these two routes:
import { Stack } from 'expo-router';
export default function Layout() {
return <Stack />;
}
Screen options and header configuration
Statically configure route options
You can use the <Stack.Screen name={routeName} />
component in the layout component route to statically configure a route's options. This is also useful for tabs or drawers as they need an icon defined ahead of time.
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}>
{/* Optionally configure static options outside the route.*/}
<Stack.Screen name="home" options={{}} />
</Stack>
);
}
As an alternative to the <Stack.Screen>
component, you can use navigation.setOptions()
to configure a route's options from within the route's component file.
import { Stack, useNavigation } from 'expo-router';
import { Text, View } from 'react-native';
import { useEffect } from 'react';
export default function Home() {
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({ headerShown: false });
}, [navigation]);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
Configure header bar
You can configure the header bar for all routes in a Stack
navigator by using the screenOptions
prop. This is useful for setting a common header style across all routes.
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
/>
);
}
To configure the header bar dynamically for an individual route, use that navigator's <Stack.Screen>
component in the routes's file. This is useful for interactions that change the UI.
import { Link, Stack } from 'expo-router';
import { Image, Text, View, StyleSheet } from 'react-native';
function LogoTitle() {
return (
<Image style={styles.image} source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }} />
);
}
export default function Home() {
return (
<View style={styles.container}>
<Stack.Screen
options={{
title: 'My home',
headerStyle: { backgroundColor: '#f4511e' },
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
headerTitle: props => <LogoTitle {...props} />,
}}
/>
<Text>Home Screen</Text>
<Link href={{ pathname: 'details', params: { name: 'Bacon' } }}>Go to Details</Link>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
image: {
width: 50,
height: 50,
},
});
Available header options
The Stack
navigator supports comprehensive header configuration options. Below are all the header-related options available:
Header options
Option | Platform | Description |
---|---|---|
header | Android iOS | Custom header to use instead of the default header. This accepts a function that returns a React Element to display as a header. The function receives an object containing the following properties as the argument:
To set a custom header for all the screens in the navigator, you can specify this option in the Note that if you specify a custom header, the native functionality such as large title, search bar etc. won't work. |
headerBackButtonDisplayMode | iOS | How the back button displays icon and title. Supported values:
The space-aware behavior is disabled when:
In such cases, a static title and icon are always displayed. |
headerBackButtonMenuEnabled | iOS | Boolean indicating whether to show the menu on longPress of iOS >= 14 back button. Defaults to |
headerBackground | Android iOS | Function which returns a React Element to render as the background of the header. This is useful for using backgrounds such as an image or a gradient. |
headerBackImageSource | Android iOS | Image to display in the header as the icon in the back button. Defaults to back icon image for the platform
|
headerBackTitle | iOS | Title string used by the back button on iOS. Defaults to the previous scene's title, "Back" or arrow icon depending on the available space. See Use |
headerBackTitleStyle | iOS | Style object for header back title. Supported properties:
|
headerBackVisible | Android iOS | Whether the back button is visible in the header. You can use it to show a back button alongside This will have no effect on the first screen in the stack. |
headerBlurEffect | iOS | Blur effect for the translucent header. The Supported values: |
headerLargeStyle | iOS | Style of the header when a large title is shown. The large title is shown if Supported properties:
|
headerLargeTitle | iOS | Whether to enable header with large title which collapses to regular header on scroll.
Defaults to For large title to collapse on scroll, the content of the screen should be wrapped in a scrollable view such as |
headerLargeTitleShadowVisible | Android iOS | Whether drop shadow of header is visible when a large title is shown. |
headerLargeTitleStyle | iOS | Style object for large title in header. Supported properties:
|
headerLeft | Android iOS | Function which returns a React Element to display on the left side of the header. This replaces the back button. See |
headerRight | Android iOS | Function which returns a React Element to display on the right side of the header. |
headerSearchBarOptions | iOS | Options to render a native search bar on iOS. Search bars are rarely static so normally it is controlled by passing an object to You also need to specify Supported properties are: ref Ref to manipulate the search input imperatively. It contains the following methods:
autoCapitalize Controls whether the text is automatically auto-capitalized as it is entered by the user. Possible values:
Defaults to autoFocus Whether to automatically focus search bar when it's shown. Defaults to barTintColor The search field background color. By default bar tint color is translucent. tintColor The color for the cursor caret and cancel button text. cancelButtonText The text to be used instead of default disableBackButtonOverride Whether the back button should close search bar's text input or not. Defaults to hideNavigationBar Boolean indicating whether to hide the navigation bar during searching. Defaults to hideWhenScrolling Boolean indicating whether to hide the search bar when scrolling. Defaults to inputType The type of the input. Defaults to Supported values: obscureBackground Boolean indicating whether to obscure the underlying content with semi-transparent overlay. Defaults to placeholder Text displayed when search field is empty. textColor The color of the text in the search field. hintTextColor The color of the hint text in the search field. headerIconColor The color of the search and close icons shown in the header shouldShowHintSearchIcon Whether to show the search hint icon when search bar is focused. Defaults to onBlur A callback that gets called when search bar has lost focus. onCancelButtonPress A callback that gets called when the cancel button is pressed. onChangeText A callback that gets called when the text changes. It receives the current text value of the search bar. |
headerShadowVisible | Android iOS | Whether to hide the elevation shadow (Android) or the bottom border (iOS) on the header. |
headerShown | Android iOS | Whether to show the header. The header is shown by default. Setting this to |
headerStyle | Android iOS | Style object for header. Supported properties:
|
headerTintColor | Android iOS | Tint color for the header. Changes the color of back button and title. |
headerTitle | Android iOS | String or a function that returns a React Element to be used by the header. Defaults to When a function is passed, it receives Note that if you render a custom element by passing a function, animations for the title won't work. |
headerTitleAlign | Android iOS | How to align the header title. Possible values:
Defaults to Not supported on iOS. It's always |
headerTitleStyle | Android iOS | Style object for header title. Supported properties:
|
headerTransparent | Android iOS | Boolean indicating whether the navigation bar is translucent. Defaults to This is useful if you want to render a semi-transparent header or a blurred background. Note that if you don't want your content to appear under the header, you need to manually add a top margin to your content. React Navigation won't do it automatically. To get the height of the header, you can use |
title | Android iOS | String that can be used as a fallback for |
Set screen options dynamically
To configure a route's option dynamically, you can always use the <Stack.Screen>
component in that route's file.
As an alternative, you can also use the imperative API's router.setParams()
function to configure the route dynamically.
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import { View, Text, StyleSheet } from 'react-native';
export default function Details() {
const router = useRouter();
const params = useLocalSearchParams();
return (
<View style={styles.container}>
<Stack.Screen
options={{
title: params.name,
}}
/>
<Text
onPress={() => {
router.setParams({ name: 'Updated' });
}}>
Update the title
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
Header buttons
You can add buttons to the header by using the headerLeft
and headerRight
options. These options accept a React component that renders in the header.
import { Stack } from 'expo-router';
import { Button, Text, Image, StyleSheet } from 'react-native';
import { useState } from 'react';
function LogoTitle() {
return (
<Image style={styles.image} source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }} />
);
}
export default function Home() {
const [count, setCount] = useState(0);
return (
<>
<Stack.Screen
options={{
headerTitle: props => <LogoTitle {...props} />,
headerRight: () => <Button onPress={() => setCount(c => c + 1)} title="Update count" />,
}}
/>
<Text>Count: {count}</Text>
</>
);
}
const styles = StyleSheet.create({
image: {
width: 50,
height: 50,
},
});
Other screen options
For a complete list of all available other screen options including animations, gestures, and other configurations:
Screen options
Option | Platform | Description |
---|---|---|
animation | Android | How the screen should animate when pushed or popped. Supported values: |
animationDuration | iOS | Changes the duration (in milliseconds) of The duration of |
animationMatchesGesture | iOS | Whether the gesture to dismiss should use animation provided to Doesn't affect the behavior of screens presented modally. |
animationTypeForReplace | Android iOS | The type of animation to use when this screen replaces another screen. Defaults to Supported values: |
autoHideHomeIndicator | iOS | Boolean indicating whether the home indicator should prefer to stay hidden. Defaults to |
contentStyle | Android iOS | Style object for the scene content. |
freezeOnBlur | iOS | Boolean indicating whether to prevent inactive screens from re-rendering. Defaults to Only supported on iOS and Android. |
fullScreenGestureEnabled | iOS | Whether the gesture to dismiss should work on the whole screen. Using gesture to dismiss with this option results in the same transition animation as Doesn't affect the behavior of screens presented modally. |
fullScreenGestureShadowEnabled | Android iOS | Whether the full screen dismiss gesture has shadow under view during transition. Defaults to This does not affect the behavior of transitions that don't use gestures enabled by |
gestureDirection | iOS | Sets the direction in which you should swipe to dismiss the screen. Supported values: When using |
gestureEnabled | iOS | Whether you can use gestures to dismiss this screen. Defaults to |
navigationBarColor | Android | This option is deprecated and will be removed in a future release (for apps targeting Android SDK 35 or above edge-to-edge mode is enabled by default and it is expected that the edge-to-edge will be enforced in future SDKs, see here for more information). Sets the navigation bar color. Defaults to initial status bar color. |
navigationBarHidden | Android | Boolean indicating whether the navigation bar should be hidden. Defaults to |
orientation | Android | The display orientation to use for the screen. Supported values: |
presentation | Android | How should the screen be presented. Supported values: |
sheetAllowedDetents | Android | Works only when Describes heights where a sheet can rest. Supported values: Defaults to |
sheetCornerRadius | Android | Works only when The corner radius that the sheet will try to render with. If set to non-negative value it will try to render sheet with provided radius, else it will apply system default. If left unset, system default is used. |
sheetElevation | Android | Works only when Integer value describing elevation of the sheet, impacting shadow on the top edge of the sheet. Not dynamic - changing it after the component is rendered won't have an effect. Defaults to |
sheetExpandsWhenScrolledToEdge | iOS | Works only when Whether the sheet should expand to larger detent when scrolling. Defaults to Please note that for this interaction to work, the ScrollView must be "first-subview-chain" descendant of the Screen component. This restriction is due to platform requirements. |
sheetGrabberVisible | iOS | Works only when Boolean indicating whether the sheet shows a grabber at the top. Defaults to |
sheetInitialDetentIndex | Android | Works only when Index of the detent the sheet should expand to after being opened. If the specified index is out of bounds of Additionaly there is Defaults to |
sheetLargestUndimmedDetentIndex | Android | Works only when The largest sheet detent for which a view underneath won't be dimmed. This prop can be set to an number, which indicates index of detent in Additionaly there are following options available:
Defaults to |
statusBarAnimation | Android | Sets the status bar animation (similar to the Supported values: On Android, setting either Requires setting |
statusBarBackgroundColor | Android | This option is deprecated and will be removed in a future release (for apps targeting Android SDK 35 or above edge-to-edge mode is enabled by default and it is expected that the edge-to-edge will be enforced in future SDKs, see here for more information). Sets the background color of the status bar (similar to the |
statusBarHidden | Android | Whether the status bar should be hidden on this screen. Requires setting |
statusBarStyle | Android | Sets the status bar color (similar to the Supported values: Requires setting |
statusBarTranslucent | Android | This option is deprecated and will be removed in a future release (for apps targeting Android SDK 35 or above edge-to-edge mode is enabled by default and it is expected that the edge-to-edge will be enforced in future SDKs, see here for more information). Sets the translucency of the status bar (similar to the |
Custom push behavior
By default, the Stack
navigator removes duplicate screens when pushing a route that is already in the stack. For example, if you push the same screen twice, the second push will be ignored. You can change this push behavior by providing a custom getId()
function to the <Stack.Screen>
.
For example, the index
route in the following layout structure shows a list of different user profiles in the app. Let's make the [details]
route a dynamic route so that the app user can navigate to see a profile's details.
app
_layout.tsx
index.tsx
[details].tsx
matches dynamic paths like '/details1'
The Stack
navigator will push a new screen every time the app user navigates to a different profile but will fail. If you provide a getId()
function that returns a new ID every time, the Stack
will push a new screen every time the app user navigates to a profile.
You can use the <Stack.Screen name="[profile]" getId={}>
component in the layout component route to modify the push behavior:
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack>
<Stack.Screen
name="[profile]"
getId={
({ params }) => String(Date.now())
}
/>
</Stack>
);
}
Removing stack screens
There are different actions you can use to dismiss and remove one or many routes from a stack.
dismiss
action
Dismisses the last screen in the closest stack. If the current screen is the only route in the stack, it will dismiss the entire stack.
You can optionally pass a positive number to dismiss up to that specified number of screens.
Dismiss is different from back
as it targets the closest stack and not the current navigator. If you have nested navigators, calling dismiss
will take you back multiple screens.
import { Button, View } from 'react-native';
import { useRouter } from 'expo-router';
export default function Settings() {
const router = useRouter();
const handleDismiss = (count: number) => {
router.dismiss(count)
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Go to first screen" onPress={() => handleDismiss(3)} />
</View>
);
}
dismissTo
action
dismissTo
was added in Expo Router4.0.8
. It operates similarly to thenavigation
function in Expo Router v3.
Dismisses screens in the current <Stack />
until the specified Href
is reached. If the Href
is absent in the history, a push
action will be performed instead.
For example, consider the history of /one
, /two
, /three
routes, where /three
is the current route. The action router.dismissTo('/one')
will cause the history to go back twice, while router.dismissTo('/four')
will push
the history forward to the /four
route.
import { Button, View, Text } from 'react-native';
import { useRouter } from 'expo-router';
export default function Settings() {
const router = useRouter();
const handleDismissAll = () => {
router.dismissTo('/')
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Go to first screen" onPress={handleDismissAll} />
</View>
);
}
dismissAll
action
To return to the first screen in the closest stack. This is similar to popToTop
stack action.
For example, the home
route is the first screen, and the settings
is the last. To go from settings
to home
route you'll have to go back to details
. However, using the dismissAll
action, you can go from settings
to home
and dismiss any screen in between.
import { Button, View, Text } from 'react-native';
import { useRouter } from 'expo-router';
export default function Settings() {
const router = useRouter();
const handleDismissAll = () => {
router.dismissAll()
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Go to first screen" onPress={handleDismissAll} />
</View>
);
}
canDismiss
action
To check if it is possible to dismiss the current screen. Returns true
if the router is within a stack with more than one screen in the stack's history.
import { Button, View } from 'react-native';
import { useRouter } from 'expo-router';
export default function Settings() {
const router = useRouter();
const handleDismiss = (count: number) => {
if (router.canDismiss()) {
router.dismiss(count)
}
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Maybe dismiss" onPress={() => handleDismiss()} />
</View>
);
}
Relation with Native Stack Navigator
The Stack
navigator in Expo Router wraps the Native Stack Navigator from React Navigation. Options available in the Native Stack Navigator are all available in the Stack
navigator in Expo Router.
JavaScript stack with @react-navigation/stack
You can also use the JavaScript-powered @react-navigation/stack
library to create a custom layout component by wrapping this library with the withLayoutContext
.
In the following example, JsStack
component is defined using @react-navigation/stack
library:
import { ParamListBase, StackNavigationState } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationEventMap,
StackNavigationOptions,
} from '@react-navigation/stack';
import { withLayoutContext } from 'expo-router';
const { Navigator } = createStackNavigator();
export const JsStack = withLayoutContext<
StackNavigationOptions,
typeof Navigator,
StackNavigationState<ParamListBase>,
StackNavigationEventMap
>(Navigator);
After defining the JsStack
component, you can use it in your app:
import { JsStack } from '../layouts/js-stack';
export default function Layout() {
return (
<JsStack
screenOptions={
{
%%placeholder-start%%... %%placeholder-end%%
}
}
/>
);
}
For more information on available options, see @react-navigation/stack
documentation.