Custom Screen Transitions in React Navigation

This guide works for react-navigation-stack (Stack Navigator for React Navigation 4) as well as @react-navigation/stack 5.0 (part of React Navigation 5).

Have you ever wanted to implement a custom transition animation for React Navigation’s stack navigator? Then you’ve come to the right place. In this guide, we’ll learn how to implement a custom screen transition animation from scratch.

The guide assumes basic knowledge about animations in React Native, specifically interpolations. If you're not familiar with interpolations, take a look at the official docs for Animated.

Stack Navigator

Recently the Stack Navigator was re-written and has a completely new API for defining transitions. It allows us to customize the transition for various parts of our screen as well as the header. The animations we define integrate seamlessly with the swipe gesture as well. We'll implement a custom animation in the guide to get familiar with the concepts.

We are aiming for the following animation:

Before writing some code, let's split the animation into small parts. During the animation/gesture, we can notice the following:

  • The focused screen moves and rotates
  • There's a overlay behind the focused screen which gets darker/lighter
  • The screen behind scales up/down
  • The header items cross-fade

Let's take a look at the official docs for Stack Navigator animations. There are 4 animation related options:

  • <rte-code>gestureDirection<rte-code>
  • <rte-code>transitionSpec<rte-code>
  • <rte-code>cardStyleInterpolator<rte-code>
  • <rte-code>headerStyleInterpolator<rte-code>

We want the screen to respond to horizontal gestures, so we'll set <rte-code>gestureDirection<rte-code> to <rte-code>horizontal<rte-code>. For <rte-code>transitionSpec<rte-code>, we need to specify the configuration to use for the transition (e.g. <rte-code>spring<rte-code> animation and some options for it). To keep things simple, let's use one of the built-in configs for this. For the header, we'll use the built-in fade animation. So for now, our custom transition looks like this:

import { TransitionSpecs, HeaderStyleInterpolators } from '@react-navigation/stack';
const MyTransition = {
gestureDirection: 'horizontal',
transitionSpec: {
open: TransitionSpecs.TransitionIOSSpec,
close: TransitionSpecs.TransitionIOSSpec,
},
headerStyleInterpolator: HeaderStyleInterpolators.forFade,
}
view raw gistfile1.txt hosted with ❤ by GitHub

To use this configuration in our navigator, we can pass it in options (React Navigation v5):

<Stack.Screen
name="Test"
component={TestScreen}
options={{
title: 'Custom animation',
...MyTransition,
}}
/>
view raw gistfile1.txt hosted with ❤ by GitHub

Or <rte-code>navigationOptions<rte-code> (Stack v2 with React Navigation v4):

function TestScreen() {
// content
}
TestScreen.navigationOptions = {
title: 'Custom animation',
...MyTransition,
};
view raw gistfile1.txt hosted with ❤ by GitHub

To use the animation for all screens, we can pass it in <rte-code>screenOptions<rte-code> (React Navigation v5) or <rte-code>defaultNavigationOptions<rte-code> (Stack v2 with React Navigation v4).

Now let's get to the interesting parts, <rte-code>cardStyleInterpolator<rte-code> and <rte-code>headerStyleInterpolator<rte-code>. So what are these?

Interpolating transitions in React

The API for transitions is based on the concept of interpolations. If you have some math background, the interpolation idea should ring a bell and it’s exactly what you should remember from your math classes. React Navigation gives us the animated nodes for the current screen in the stack (<rte-code>current.progress<rte-code>) and screen after that one (<rte-code>next.progress<rte-code>). The <rte-code>progress<rte-code> values are the animation/gesture progress. When the screen in first starts opening, the value will be <rte-code>0<rte-code> and when it's fully open, the value will be <rte-code>1<rte-code>. We can interpolate on those and specify styles for various parts of the screen.

So let's write the styles for the card (the part of the screen which contains the content) first. We want our focused card to rotate and translate based on the <rte-code>progress<rte-code> value of the card.

{
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
{
rotate: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
},
],
}
view raw gistfile1.txt hosted with ❤ by GitHub

Here, the card starts from a translate value of screen width and 1 radian rotation to no translate and zero rotation.

We also want the previous card to scale down. If we have a next value, we know that the card is behind another card, so we need to scale it down based on the next card's progress:

{
transform: [
{
scale: next
? next.progress.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.9],
})
: 1,
},
],
}
view raw gistfile1.txt hosted with ❤ by GitHub

Next part is the overlay behind the card. It's black by default, so we just need to change its opacity:

{
opacity: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.5],
}),
}
view raw gistfile1.txt hosted with ❤ by GitHub

Now let's put it all together in our custom transition under <rte-code>cardStyleInterpolator<rte-code>. The styles for the card will be in <rte-code>cardStyle<rte-code> and for overlay, it'll be <rte-code>overlayStyle<rte-code>:

const MyTransition = {
gestureDirection: 'horizontal',
transitionSpec: {
open: TransitionSpecs.TransitionIOSSpec,
close: TransitionSpecs.TransitionIOSSpec,
},
headerStyleInterpolator: HeaderStyleInterpolators.forFade,
cardStyleInterpolator: ({ current, next, layouts }) => {
return {
cardStyle: {
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
{
rotate: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
},
{
scale: next
? next.progress.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.9],
})
: 1,
},
],
},
overlayStyle: {
opacity: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.5],
}),
},
};
},
}
view raw gistfile1.txt hosted with ❤ by GitHub

And we're done! You can check the complete demo at snack and run it on your device.

We have only implemented the card animation here, but the same principles apply for the header as well. For more details on what options are available, check the official docs.

Do you have any questions? Drop a comment below or tweet to me @satya164. Can't wait to see what animations you come up with!

As part of The React Native Show, we released a podcast fully dedicated to future of React Navigation.

Insights

Learn more about Animations

cover

Jul 8Podcast

React Native Ease: Platform-Native Animations Without the Extra Work

Łukasz talks with Janic from App & Flow about React Native Ease, Core Animation, Reanimated trade-offs, and performant UI animation in React Native.

cover

Jul 7Podcast

Where Reanimated Goes Next

A conversation with Bartłomiej Błoniarz on CSS animations in Reanimated, native primitives on iOS, shared element transitions, and the work still left on stability.

cover

Oct 30Live Stream

Reanimated, TanStack, Cross-Pollinating Languages & TWIR #256 Anniversary

Catch up on everything from TanStack updates and Astro insights to Reanimated deep dive and even Swift for Android. Plus, we celebrate the 256th newsletter!

Events Cover

Apr 16Talk

How to Make Your React (Native) App Feel Native

How to make your React Native app feel truly native, across iOS, Android, tvOS & more. Dive into best practices, navigation, and react-native-bottom-tabs.

cover

Feb 19Podcast

Reanimated 4 is the Future of Smooth React Native Animations

Reanimated 4 is now available in beta, and if you work with animations in React Native, you need to know what’s new!

cover

Oct 11Article

A Deep Dive Into React Native Reanimated v1

Read the article to learn more about what React Native Reanimated is, how it works under the hood, and what idea stands behind the library.

cover

Apr 15Article

ReanimatedArc - Build Circular Animated UI Elements in React Native

Build good-looking and smooth animated arcs in React Native with ReanimatedArc library.

cover

Aug 8Article

Reanimating Your React Native Experience

Read how to achieve delightful developer experience with react-native-gesture and react-native-animated.

cover

Jan 17Article

React Native Animations Revisited - Part III

Learn how to respond to screen gestures with PanResponder. Check the last part of our Callstack series on React Native animations!

cover

Jan 10Article

React Native Animations Revisited - Part II

In this part of the three-part guide about animations in React Native we cover creating animations with Animated API.

cover

Jan 4Article

React Native Animations Revisited - Part I

Three-part guide for developers explaining how to deal with animations in React Native - Part 1.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.