Skip to main content
This guide will walk you through creating a stack navigator with custom transitions. You’ll learn how to use presets, customize animations, and add gesture support.

Create a Basic Stack

Start by creating a stack navigator using the Blank Stack (recommended for most apps):

React Navigation

App.tsx
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import Transition from "react-native-screen-transitions";
import { View, Text, Button } from "react-native";

const Stack = createBlankStackNavigator();

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Detail"
        onPress={() => navigation.navigate('Detail')}
      />
    </View>
  );
}

function DetailScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Detail Screen</Text>
    </View>
  );
}

function App() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen
        name="Detail"
        component={DetailScreen}
        options={{
          ...Transition.Presets.SlideFromBottom(),
        }}
      />
    </Stack.Navigator>
  );
}

export default App;

Expo Router

For Expo Router, set up the stack in your layout file:
1

Create the Stack component

Create a reusable Stack component using withLayoutContext:
app/stack.tsx
import { withLayoutContext } from "expo-router";
import {
  createBlankStackNavigator,
  type BlankStackNavigationOptions,
} from "react-native-screen-transitions/blank-stack";

const { Navigator } = createBlankStackNavigator();

export const Stack = withLayoutContext<
  BlankStackNavigationOptions,
  typeof Navigator
>(Navigator);
2

Use the Stack in your layout

Import and use the Stack in your root layout:
app/_layout.tsx
import Transition from "react-native-screen-transitions";
import { Stack } from "./stack";

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen
        name="detail"
        options={{
          ...Transition.Presets.SlideFromBottom(),
        }}
      />
    </Stack>
  );
}
3

Create your screens

Create screen components in separate files:
app/index.tsx
import { View, Text, Button } from "react-native";
import { router } from "expo-router";

export default function HomeScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Detail"
        onPress={() => router.push('/detail')}
      />
    </View>
  );
}
app/detail.tsx
import { View, Text } from "react-native";

export default function DetailScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Detail Screen</Text>
    </View>
  );
}

Use Built-in Presets

The library includes several ready-made transition presets. Apply them using the spread operator:
import Transition from "react-native-screen-transitions";

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={{
    ...Transition.Presets.SlideFromBottom(),
  }}
/>

Available Presets

PresetDescription
SlideFromTop()Slides in from the top of the screen
SlideFromBottom()Slides in from bottom (modal-style)
ZoomIn()Scales in with fade effect
DraggableCard()Multi-directional drag with scaling
ElasticCard()Elastic drag with overlay backdrop
SharedIGImage({ sharedBoundTag })Instagram-style shared image transition
SharedAppleMusic({ sharedBoundTag })Apple Music-style shared element
SharedXImage({ sharedBoundTag })X (Twitter)-style image transition
Shared element presets require @react-native-masked-view/masked-view to be installed. See the Installation guide for setup instructions.

Add Gesture Support

Enable swipe-to-dismiss by adding gesture options:
<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={{
    gestureEnabled: true,
    gestureDirection: "vertical",
    ...Transition.Presets.SlideFromBottom(),
  }}
/>

Gesture Directions

Control which direction triggers the dismiss gesture:
// Swipe down to dismiss
gestureDirection: "vertical"

// Swipe up to dismiss
gestureDirection: "vertical-inverted"

// Swipe left to dismiss
gestureDirection: "horizontal"

// Swipe right to dismiss
gestureDirection: "horizontal-inverted"

// Any direction dismisses
gestureDirection: "bidirectional"

// Multiple directions
gestureDirection: ["horizontal", "vertical"]

Gesture Activation Areas

Control where gestures can start:
// Only from screen edges (recommended)
gestureActivationArea: "edge"

// Anywhere on screen
gestureActivationArea: "screen"

// Per-side configuration
gestureActivationArea: {
  left: "edge",
  right: "screen",
  top: "edge",
  bottom: "screen",
}

Create Custom Animations

Define your own transitions using the screenStyleInterpolator function. Every screen has a progress value that animates from 0 → 1 → 2:
0 ─────────── 1 ─────────── 2
entering     visible      exiting

Simple Fade Transition

import { interpolate } from "react-native-reanimated";

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={{
    screenStyleInterpolator: ({ progress }) => {
      "worklet";
      return {
        contentStyle: {
          opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
        },
      };
    },
  }}
/>

Slide from Right

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={{
    screenStyleInterpolator: ({ progress, layouts: { screen } }) => {
      "worklet";
      return {
        contentStyle: {
          transform: [{
            translateX: interpolate(
              progress,
              [0, 1, 2],
              [screen.width, 0, -screen.width * 0.3]
            ),
          }],
        },
      };
    },
  }}
/>

Zoom with Fade

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={{
    screenStyleInterpolator: ({ progress }) => {
      "worklet";
      return {
        contentStyle: {
          opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
          transform: [{
            scale: interpolate(progress, [0, 1, 2], [0.9, 1, 0.9]),
          }],
        },
      };
    },
  }}
/>
Always include the "worklet"; directive at the start of your interpolator function. This tells Reanimated to run the code on the UI thread for optimal performance.

Control Animation Timing

Customize the animation speed using spring configurations:
<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={{
    screenStyleInterpolator: myInterpolator,
    transitionSpec: {
      open: { stiffness: 1000, damping: 500, mass: 3 },    // Screen enters
      close: { stiffness: 1000, damping: 500, mass: 3 },   // Screen exits
    },
  }}
/>

Spring Configuration Options

PropertyDescriptionDefault
stiffnessSpring stiffness (higher = faster)1000
dampingSpring damping (higher = less bounce)500
massSpring mass (higher = slower)3

Use Transition-Aware ScrollViews

When using ScrollViews with gestures, use the transition-aware components:
import Transition from "react-native-screen-transitions";

function DetailScreen() {
  return (
    <Transition.ScrollView>
      <Text>Scrollable content...</Text>
    </Transition.ScrollView>
  );
}
Also available:
  • Transition.FlatList - For lists
  • Transition.View - For shared elements
  • Transition.Pressable - For shared element sources
These components automatically coordinate with the navigation gesture system. Vertical gestures only activate when scrolled to the top (or bottom for inverted gestures).

Complete Example

Here’s a complete example combining multiple features:
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import Transition from "react-native-screen-transitions";
import { View, Text, Button } from "react-native";
import { interpolate } from "react-native-reanimated";

const Stack = createBlankStackNavigator();

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text style={{ fontSize: 24, marginBottom: 20 }}>Home</Text>
      <Button
        title="Show Modal"
        onPress={() => navigation.navigate('Modal')}
      />
    </View>
  );
}

function ModalScreen() {
  return (
    <View style={{ flex: 1, backgroundColor: 'white' }}>
      <Transition.ScrollView
        contentContainerStyle={{ padding: 20 }}
      >
        <Text style={{ fontSize: 24, marginBottom: 20 }}>Modal Screen</Text>
        <Text>Swipe down to dismiss...</Text>
        {/* More content */}
      </Transition.ScrollView>
    </View>
  );
}

function App() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen
        name="Modal"
        component={ModalScreen}
        options={{
          gestureEnabled: true,
          gestureDirection: "vertical",
          gestureActivationArea: "edge",
          ...Transition.Presets.SlideFromBottom(),
        }}
      />
    </Stack.Navigator>
  );
}

export default App;

Next Steps

You’ve learned the basics! Explore more advanced features:

Shared Elements

Create smooth element transitions between screens

Snap Points

Build multi-stop bottom sheets and side panels

Presets

Explore all built-in transition presets

Custom Animations

Learn advanced animation techniques