expo-transition-router
v1.0.1
Published
High-performance, asynchronous page transitions for Expo Router.
Maintainers
Readme
Easily add high-performance, asynchronous page transitions to your Expo Router apps. Built for speed, flexibility, and a premium "native" feel.
Features
- ✅ Async Transitions: Support for custom
leaveandenteranimations (perfect for Reanimated or Moti). - ✅ Expo Router Integration: Seamless wrapper around the standard Expo Router API.
- ✅ Dynamic Animation Selection: Choose between different animation styles per navigation call.
- ✅ History-Aware Back Support: Automatically remember which animation was used to enter a screen and replay it on exit.
- ✅ Hardware Back Button Support: Built-in integration with Android's
BackHandler. - ✅ Hot Reload Resilience: Smart lifecycle detection ensures animations don't trigger during development reloads.
- ✅ Zero Faint/Flashes: Prevents common screen unmounting flashes by orchestrating the handoff between screens.
Installation
npm install expo-transition-router
# Ensure you have the required peer dependencies
npx expo install expo-router react-native-reanimatedQuick Start
1. Wrap your Root Layout
In your app/_layout.jsx, wrap your root component with the TransitionProvider.
import { TransitionProvider } from 'expo-transition-router';
import { Stack } from 'expo-router';
export default function Layout() {
return (
<TransitionProvider
leave={(next) => setTimeout(next, 500)} // Orchestrate navigation timing
enter={(next) => setTimeout(next, 500)}
defaultAnimation="staggered"
backBehavior="pop"
>
<Stack screenOptions={{ headerShown: false }} />
{/* Add your overlay component here */}
<TransitionManager />
</TransitionProvider>
);
}2. Use the Custom Router
Instead of useRouter from expo-router, use useTransitionRouter to trigger transitions.
import { useTransitionRouter } from 'expo-transition-router';
export function Home() {
const router = useTransitionRouter();
return (
<Button
title="Go to About"
onPress={() => router.push('/about', { animation: 'circular' })}
/>
);
}API Reference
TransitionProvider
The core provider that manages transition state.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| leave | TransitionCallback | next => next() | Async function called before navigation. |
| enter | TransitionCallback | next => next() | Async function called after navigation. |
| defaultAnimation| string | 'staggered' | The fallback animation key. |
| backBehavior | 'pop' \| 'default' | 'pop' | pop remembers the enter animation; default always uses the global style. |
useTransitionRouter
A hook that returns an object with push, replace, and back methods, compatible with Expo Router.
const router = useTransitionRouter();
router.push('/target', { animation: 'grid' });Link
A drop-in replacement for @expo/router's Link.
import { Link } from 'expo-transition-router';
<Link href="/test">Transition Link</Link>Advanced: History-Aware Transitions
If backBehavior="pop" is enabled (default), the library maintains an internal stack of animations. When router.back() is called, it re-plays the same style that was used to enter the current page.
Creating Custom Animations
You can build your own transition components by listening to the library's internal state. This allows for high-performance, complex overlays using libraries like Reanimated or Moti.
1. Understanding the Transition Lifecycle
The navigation process is split into three distinct phases:
leaving: Triggered when a navigation action starts. The library waits for yourleave()callback to finish before executing the actual route change.entering: Triggered immediately after the route has changed and the new screen is mounted. The library waits for yourenter()callback to finish.none: The steady state when no navigation is occurring.
2. The useTransitionState() Hook
This hook provides the essential state for your transition components:
const { stage, activeAnimation, isBack } = useTransitionState();| Property | Type | Description |
| --- | --- | --- |
| stage | 'none' \| 'leaving' \| 'entering' | The current lifecycle phase. |
| activeAnimation | string | The animation key requested (e.g., 'circular'). |
| isBack | boolean | true if the transition was triggered by a back navigation. |
Step-by-Step Guide: Building a "Circular Reveal"
To create a custom transition that actually works, follow these steps:
Step 1: Configure the TransitionProvider
Ensure your leave and enter callback durations match your animation's speed.
<TransitionProvider
leave={(next) => setTimeout(next, 600)} // Must match animation duration
enter={(next) => setTimeout(next, 600)}
>
<Stack />
<TransitionManager /> {/* Your custom overlay component */}
</TransitionProvider>Step 2: Create a Transition Component
Initialize your shared values and use useEffect to trigger animations when the stage changes.
import { useTransitionState } from 'expo-transition-router';
import Animated, { useAnimatedStyle, useSharedValue, withTiming, Easing } from 'react-native-reanimated';
export function CircularReveal() {
const { stage } = useTransitionState();
const scale = useSharedValue(0);
const opacity = useSharedValue(0);
useEffect(() => {
const config = { duration: 600, easing: Easing.bezier(0.4, 0, 0.2, 1) };
if (stage === 'leaving') {
opacity.value = 1;
scale.value = withTiming(1, config);
} else if (stage === 'entering') {
scale.value = withTiming(0, config, () => {
opacity.value = 0;
});
}
}, [stage]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
if (stage === 'none') return null;
return (
<Animated.View
pointerEvents="none"
style={[{ position: 'absolute', inset: 0, zIndex: 9999 }, animatedStyle]}
/>
);
}Step 3: Implement the Transition Manager
Use the activeAnimation property to switch between different animation styles dynamically.
export function TransitionManager() {
const { stage, activeAnimation } = useTransitionState();
if (stage === 'none') return null;
switch (activeAnimation) {
case 'circular': return <CircularReveal />;
default: return <DefaultFade />;
}
}Flexibility & Opting Out
1. Bypassing Transitions
If you want to skip animations for a specific navigation, you can either:
- Use the original
useRouterfromexpo-router. - Pass
animation: 'none'to theuseTransitionRoutermethods.
router.push('/test', { animation: 'none' }); // Instant navigation2. Android Back Button
By default, the library intercepts the hardware back button to play a "leave" transition. You can opt-out of this by setting autoHandleAndroidBack={false} on the TransitionProvider.
<TransitionProvider autoHandleAndroidBack={false}>
{/* Components */}
</TransitionProvider>License
MIT
