In React Navigation 7, how do you choose between the native stack and the JS stack navigator, and what does each trade away?
answer
- react-native-screens native primitives
- platform transitions, header and gestures
- cardStyleInterpolator and TransitionPresets
- large titles, search bar, form sheets
- customisation versus native feel
basics
~20 sThe native stack uses react-native-screens' native navigation primitives, so transitions, header and gestures behave like the platform's own. The JS stack re-implements them in JavaScript, trading that native feel for fully customisable transitions and headers.
solid answer
~40 s`@react-navigation/native-stack` (`createNativeStackNavigator`) renders screens through `react-native-screens`' native primitives, so you get the platform's own push animation, back-swipe, header and extras such as `headerLargeTitleEnabled` and `headerSearchBarOptions` on iOS and sheet presentations like `presentation: 'formSheet'` with `sheetAllowedDetents`. The cost is flexibility: animations are chosen from a fixed list (`'default'`, `'fade'`, `'slide_from_bottom'` and so on) and the header is configured rather than drawn. `@react-navigation/stack` (`createStackNavigator`) implements the stack in JavaScript on top of `react-native-gesture-handler`, which it lists as a peer dependency, and exposes `cardStyleInterpolator`, `transitionSpec`, `TransitionPresets` and a fully custom `header`, at the cost of platform behaviour you now own. Default to the native stack, as React Native's own navigation guide does; pick the JS stack when a design needs transitions the native one cannot express.
code
tsx · 26 linesimport { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Text } from 'react-native';
function BookScreen() {
return <Text>Book details</Text>;
}
function ChaptersScreen() {
return <Text>Chapters</Text>;
}
export const LibraryStack = createNativeStackNavigator({
screenOptions: {
headerLargeTitleEnabled: true,
},
screens: {
Book: BookScreen,
Chapters: {
screen: ChaptersScreen,
options: {
presentation: 'formSheet',
sheetAllowedDetents: [0.5, 1],
sheetGrabberVisible: true,
},
},
},
});go deeper
Recall the two packages and factories, and that the native stack uses native screens while the JS stack animates in JavaScript.
Explain what each trades: native transitions, header, large titles and sheets versus interpolators, presets and custom headers, plus the JS stack's gesture-handler dependency.
Make the call per flow: native stack by default, JS stack only where a design needs it, and know which native options are iOS-only or behave differently on Android.
Set the default for the codebase and the bar for exceptions, balancing brand-specific motion against platform consistency and maintenance.
## Two implementations of the same idea Both navigators give you the same **stack** model: `navigate`, `push` and `goBack`, a header with a back button, and screens that stay mounted underneath the one on top. They differ in **who draws the stack**. | | Native stack | JS stack | |---|---|---| | Package | `@react-navigation/native-stack` | `@react-navigation/stack` | | Factory | `createNativeStackNavigator` | `createStackNavigator` | | Built on | `react-native-screens` native primitives | JavaScript views, animations and `react-native-gesture-handler` | | Transitions | Platform's own, chosen from a list | Any, via interpolators and specs | | Header | Native header configured by options | React header you can replace entirely | ## The native stack The native stack hands each screen to `react-native-screens`, which uses the platform's own navigation containers. Consequences: - **It looks and behaves native by default**: the iOS push animation and edge back-swipe, Android's platform transition, and a header that behaves like a native navigation bar. - **Platform extras are available as options**: `headerLargeTitleEnabled` for iOS large titles that collapse on scroll (the older `headerLargeTitle` is deprecated), `headerSearchBarOptions` for a native search bar, `headerBlurEffect` and `headerTransparent`. - **Sheet presentations**: `presentation: 'formSheet'` with `sheetAllowedDetents` (fractions of the screen height, or `'fitToContents'`) and `sheetGrabberVisible` gives a native resizable sheet; the docs note Android accepts at most three detents. - **`animation` takes a fixed set of values**: `'default'`, `'fade'`, `'fade_from_bottom'`, `'flip'` (iOS, needs `presentation: 'modal'`), `'simple_push'` (iOS), `'slide_from_bottom'`, `'slide_from_right'` and `'slide_from_left'` (Android), `'ios_from_right'` and `'ios_from_left'` (Android), and `'none'`. The trade-off is that you configure rather than program: an arbitrary shared-axis or 3D transition is not on the list. ## The JS stack The JS stack renders cards and headers as React Native views and animates them itself: - **Transitions are code**: `cardStyleInterpolator` maps progress to styles, `transitionSpec` picks timing or spring, and `TransitionPresets` bundles platform-like presets such as `SlideFromRightIOS`, `ModalPresentationIOS` and `FadeFromBottomAndroid`. - **Headers are components**: pass `header` to render anything, and `headerMode` chooses `'float'` (one header animating across screens; the default on iOS for non-modals) or `'screen'` (a header per screen). - **`presentation`** is `'card'` (default), `'modal'` or `'transparentModal'`; the modal values switch `headerMode` to `'screen'` and change the animation. - **Gestures come from `react-native-gesture-handler`**, a peer dependency you must install and set up. Because it re-implements platform behaviour in JavaScript, fine details can drift from what the OS does, and the team owns the result. ## Setup differences - **Both** build on `@react-navigation/native` and list `react-native-screens` (4 or newer) and `react-native-safe-area-context` as peer dependencies. - **The JS stack also needs `react-native-gesture-handler`** (2 or newer), because its back-swipe and modal-dismiss gestures are JavaScript gesture handlers. - **The native stack's extras are native**: large titles, the search bar and sheets come from `react-native-screens`, so they need a native rebuild when that library is upgraded, not just a JavaScript reload. ## Choosing 1. **Start with the native stack.** It is what React Native's navigation guide installs, it matches platform conventions, and native-only features like large titles and form sheets are one option away. 2. **Switch a stack to the JS implementation** when a design requires transitions or headers the native stack cannot express, such as a custom interpolated card animation, and the team accepts owning that behaviour. 3. **Mixing is allowed**: different stacks in one app can use different implementations, since each is just a navigator. ## An audiobook example For an audiobook app, the stack that pushes a book's details and chapter list is a natural native stack, and the chapter picker can be a native `formSheet` with detents at half and full height. A marketing carousel with a bespoke page-curl transition between screens would be a reason to reach for the JS stack in that one flow. ## Common misconceptions - **"The JS stack is the only one with modals."** The native stack supports `presentation: 'modal'`, `'transparentModal'`, `'fullScreenModal'`, `'formSheet'` and more. - **"The native stack can't hide its header."** `headerShown: false` works on both. - **"The JS stack needs no extra setup."** It depends on `react-native-gesture-handler`.
- A designer wants a custom cross-fade-and-scale transition between two screens. Which stack, and how?The JS stack: its `cardStyleInterpolator` receives the transition progress and returns styles for the card, so a fade plus scale is a few lines, with `transitionSpec` choosing timing or spring. The native stack only offers its fixed `animation` values, and none of them is a scale.
- Why can a native stack's large title fail to collapse when the list scrolls?The `headerLargeTitleEnabled` option documents two conditions: the scrollable content must fill the screen, and the `ScrollView` or `FlatList` needs `contentInsetAdjustmentBehavior="automatic"`. It is also iOS-only, so Android shows a regular header.
saying these in an interview costs you the question
- The native stack and the JS stack are the same package with a flag.
- The native stack cannot present modals, so modals need the JS stack.
- The JS stack needs no react-native-gesture-handler setup.
- cardStyleInterpolator also customises native stack transitions.
- The native stack's animation option accepts any custom animation function.