Lesson 15

Custom Transitions and Animations

The native stack navigator handles transitions automatically — a right-to-left slide on iOS, a bottom-to-top fade on Android. But you can change the animation for any screen or the entire navigator.

Built-in Animations

The animation screen option controls the transition. The native stack supports several presets:

<Stack.Screen
  name="Details"
  component={DetailsScreen}
  options={{ animation: 'slide_from_bottom' }}
/>

Available values:

  • 'default' — platform default (slide on iOS, fade on Android)
  • 'slide_from_right' — right-to-left slide
  • 'slide_from_left' — left-to-right slide
  • 'slide_from_bottom' — bottom-to-top slide (like a modal)
  • 'fade' — simple fade transition
  • 'fade_from_bottom' — fade combined with an upward slide (Android default)
  • 'flip' — a flip animation (iOS only)
  • 'none' — no animation, instant transition

Per-Screen Animations

Apply different animations to different screens:

<Stack.Navigator>
  <Stack.Screen name="Home" component={HomeScreen} />
  <Stack.Screen
    name="Details"
    component={DetailsScreen}
    options={{ animation: 'slide_from_bottom' }}
  />
  <Stack.Screen
    name="Settings"
    component={SettingsScreen}
    options={{ animation: 'fade' }}
  />
</Stack.Navigator>

Details slides up from the bottom, Settings fades in, and Home uses the platform default.

Navigator-Wide Animation

Apply the same animation to all screens in a navigator:

<Stack.Navigator screenOptions={{ animation: 'fade' }}>
  <Stack.Screen name="Home" component={HomeScreen} />
  <Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>

Individual screen options override the navigator-level setting.

Animation Duration

Control how long the transition takes:

<Stack.Screen
  name="Details"
  component={DetailsScreen}
  options={{
    animation: 'slide_from_right',
    animationDuration: 200,
  }}
/>

animationDuration is in milliseconds. Lower values feel snappier. The default varies by platform but is around 350ms.

Custom Transition with animationTypeForReplace

When using conditional screens (like the auth flow from a previous lesson), animationTypeForReplace controls the animation when one screen replaces another:

<Stack.Screen
  name="Login"
  component={LoginScreen}
  options={{ animationTypeForReplace: 'pop' }}
/>

Setting it to 'pop' makes the login screen animate as if it's being popped off the stack (sliding right/down) rather than being pushed (sliding left/up). This feels more natural when transitioning from login to the main app.

Disabling Gestures

By default, users can swipe back on iOS to pop a screen. Disable this for screens where you don't want gesture-based dismissal:

<Stack.Screen
  name="Checkout"
  component={CheckoutScreen}
  options={{ gestureEnabled: false }}
/>

Complete Example

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { View, Text, Button, StyleSheet } from 'react-native';

const Stack = createNativeStackNavigator();

function HomeScreen({ navigation }) {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Home</Text>
      <Button title="Details (slide up)" onPress={() => navigation.navigate('Details')} />
      <Button title="Settings (fade)" onPress={() => navigation.navigate('Settings')} />
      <Button title="Alert (flip)" onPress={() => navigation.navigate('Alert')} />
    </View>
  );
}

function DetailsScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Details</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Settings</Text>
    </View>
  );
}

function AlertScreen({ navigation }) {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Alert!</Text>
      <Button title="Dismiss" onPress={() => navigation.goBack()} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 },
  title: { fontSize: 24, marginBottom: 16 },
});

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen
          name="Details"
          component={DetailsScreen}
          options={{ animation: 'slide_from_bottom', animationDuration: 200 }}
        />
        <Stack.Screen
          name="Settings"
          component={SettingsScreen}
          options={{ animation: 'fade' }}
        />
        <Stack.Screen
          name="Alert"
          component={AlertScreen}
          options={{ animation: 'flip', gestureEnabled: false }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Each screen demonstrates a different transition — slide from bottom, fade, and flip. Experiment with the values to find what fits your app.

In the next lesson we'll add TypeScript types to our navigation for safety and autocompletion.