https://github.com/eds2002/react-native-screen-transitions
Easy screen transitions 😎
https://github.com/eds2002/react-native-screen-transitions
Last synced: 4 days ago
JSON representation
Easy screen transitions 😎
- Host: GitHub
- URL: https://github.com/eds2002/react-native-screen-transitions
- Owner: eds2002
- License: mit
- Created: 2025-07-11T21:17:21.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2026-03-30T04:48:14.000Z (7 days ago)
- Last Synced: 2026-03-30T06:57:03.010Z (7 days ago)
- Language: TypeScript
- Homepage:
- Size: 156 MB
- Stars: 1,310
- Watchers: 7
- Forks: 35
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
- awesome-react-native - react-native-screen-transitions - Gesture-driven screen transitions with shared elements and ready-made presets. (Animation & Gestures / Graphics & Drawing)
README
# react-native-screen-transitions
Customizable screen transitions for React Native. Build gesture-driven, shared element, and fully custom animations with a simple API.
| iOS | Android |
| --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| | |
## Features
- **Full Animation Control** – Define exactly how screens enter, exit, and respond to gestures
- **Shared Elements** – Smooth transitions between screens using the Bounds API
- **Gesture Support** – Swipe-to-dismiss with edge or full-screen activation
- **Stack Progress** – Track animation progress across the entire stack
- **Ready-Made Presets** – Instagram, Apple Music, X (Twitter) style transitions included
## When to Use This Library
| Use Case | This Library | Alternative |
|----------|--------------|-------------|
| Custom transitions (slide, zoom, fade variations) | Yes | `@react-navigation/stack` works too |
| Shared element transitions | **Yes** | Limited options elsewhere |
| Multi-stop sheets (bottom, top, side) with snap points | **Yes** | Dedicated sheet libraries |
| Gesture-driven animations (drag to dismiss, elastic) | **Yes** | Requires custom implementation |
| Instagram/Apple Music/Twitter-style transitions | **Yes** | Custom implementation |
| Simple push/pop with platform defaults | Overkill | `@react-navigation/native-stack` |
| Maximum raw performance on low-end devices | Not ideal | `@react-navigation/native-stack` |
**Choose this library when** you need custom animations, shared elements, or gesture-driven transitions that go beyond platform defaults.
**Choose native-stack when** you want platform-native transitions with zero configuration and maximum performance on low-end Android devices.
## Installation
```bash
npm install react-native-screen-transitions
```
### Peer Dependencies
```bash
npm install react-native-reanimated react-native-gesture-handler \
@react-navigation/native @react-navigation/native-stack \
@react-navigation/elements react-native-screens \
react-native-safe-area-context
```
---
## Quick Start
### 1. Create a Stack
```tsx
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import Transition from "react-native-screen-transitions";
const Stack = createBlankStackNavigator();
function App() {
return (
);
}
```
### 2. With Expo Router
```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);
```
---
## Presets
Use built-in presets for common transitions:
```tsx
```
| Preset | Description |
| -------------------------------------- | --------------------------------------- |
| `SlideFromTop()` | Slides in from top |
| `SlideFromBottom()` | Slides in from bottom (modal-style) |
| `ZoomIn()` | Scales in with fade |
| `DraggableCard()` | Multi-directional drag with scaling |
| `ElasticCard()` | Elastic drag with overlay |
| `SharedIGImage({ sharedBoundTag })` | Instagram-style shared image |
| `SharedAppleMusic({ sharedBoundTag })` | Apple Music-style shared element |
| `SharedXImage({ sharedBoundTag })` | X (Twitter)-style image transition |
---
## Custom Animations
### The Basics
Every screen has a `progress` value that goes from 0 → 1 → 2:
```
0 ─────────── 1 ─────────── 2
entering visible exiting
```
When navigating from A to B:
- **Screen B**: progress goes `0 → 1` (entering)
- **Screen A**: progress goes `1 → 2` (exiting)
### Simple Fade
```tsx
options={{
screenStyleInterpolator: ({ progress }) => {
"worklet";
return {
contentStyle: {
opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
},
};
},
}}
```
### Slide from Right
```tsx
options={{
screenStyleInterpolator: ({ progress, layouts: { screen } }) => {
"worklet";
return {
contentStyle: {
transform: [{
translateX: interpolate(
progress,
[0, 1, 2],
[screen.width, 0, -screen.width * 0.3]
),
}],
},
};
},
}}
```
### Slide from Bottom
```tsx
options={{
screenStyleInterpolator: ({ progress, layouts: { screen } }) => {
"worklet";
return {
contentStyle: {
transform: [{
translateY: interpolate(progress, [0, 1], [screen.height, 0]),
}],
},
};
},
}}
```
### Return Styles
Your interpolator can return:
```tsx
return {
contentStyle: { ... }, // Main screen
backdropStyle: { ... }, // Semi-transparent backdrop
["my-id"]: { ... }, // Specific element via styleId
};
```
### Animation Specs
Control timing with spring configs:
```tsx
options={{
screenStyleInterpolator: myInterpolator,
transitionSpec: {
open: { stiffness: 1000, damping: 500, mass: 3 }, // Screen enters
close: { stiffness: 1000, damping: 500, mass: 3 }, // Screen exits
expand: { stiffness: 300, damping: 30 }, // Snap point increases
collapse: { stiffness: 300, damping: 30 }, // Snap point decreases
},
}}
```
---
## Gestures
Enable swipe-to-dismiss:
```tsx
options={{
gestureEnabled: true,
gestureDirection: "vertical",
...Transition.Presets.SlideFromBottom(),
}}
```
### Gesture Options
| Option | Description |
| ------------------------- | ------------------------------------------------------------------------ |
| `gestureEnabled` | Enable swipe-to-dismiss (snap sheets: `false` blocks dismiss-to-0 only) |
| `gestureDirection` | Direction(s) for swipe gesture |
| `gestureActivationArea` | Where gesture can start |
| `gestureResponseDistance` | Pixel threshold for activation |
| `gestureVelocityImpact` | How much velocity affects dismissal (default: 0.3) |
| `gestureDrivesProgress` | Whether gesture controls animation progress (default: true) |
| `snapVelocityImpact` | How much velocity affects snap targeting (default: 0.1, lower = iOS-like)|
| `expandViaScrollView` | Allow expansion from ScrollView at boundary (default: true) |
| `gestureSnapLocked` | Lock gesture-based snap movement to current snap point |
| `backdropBehavior` | Touch handling for backdrop area |
| `backdropComponent` | Custom backdrop component (replaces default backdrop + press behavior) |
### Gesture Direction
```tsx
gestureDirection: "horizontal" // swipe left to dismiss
gestureDirection: "horizontal-inverted" // swipe right to dismiss
gestureDirection: "vertical" // swipe down to dismiss
gestureDirection: "vertical-inverted" // swipe up to dismiss
gestureDirection: "bidirectional" // any direction
// Or combine multiple:
gestureDirection: ["horizontal", "vertical"]
```
### Gesture Activation Area
```tsx
// Simple - same for all edges
gestureActivationArea: "edge" // only from screen edges
gestureActivationArea: "screen" // anywhere on screen
// Per-side configuration
gestureActivationArea: {
left: "edge",
right: "screen",
top: "edge",
bottom: "screen",
}
```
### With ScrollViews
Use transition-aware scrollables so gestures work correctly:
```tsx
{/* content */}
```
Gesture rules with scrollables:
- **vertical** – only activates when scrolled to top
- **vertical-inverted** – only activates when scrolled to bottom
- **horizontal** – only activates at left/right scroll edges
---
## Snap Points
Create multi-stop sheets that snap to defined positions. Works with any gesture direction (bottom sheets, top sheets, side sheets):
### Basic Configuration
```tsx
// Bottom sheet (most common)
// Side sheet (same API, different direction)
```
### Options
| Option | Description |
| ------------------ | -------------------------------------------------------------------- |
| `snapPoints` | Array of fractions (0-1) where sheet can rest |
| `initialSnapIndex` | Index of initial snap point (default: 0) |
| `gestureSnapLocked` | Locks gesture snapping to current point (programmatic `snapTo` still works) |
| `backdropBehavior` | Touch handling: `"block"`, `"passthrough"`, `"dismiss"`, `"collapse"`|
| `backdropComponent` | Custom backdrop component; replaces default backdrop + tap handling |
#### backdropBehavior Values
| Value | Description |
| --------------- | ---------------------------------------------------------------- |
| `"block"` | Backdrop catches all touches (default) |
| `"passthrough"` | Touches pass through to content behind |
| `"dismiss"` | Tapping backdrop dismisses the screen |
| `"collapse"` | Tapping backdrop collapses to next lower snap point, then dismisses |
#### Custom Backdrop Component
Use `backdropComponent` when you want full control over backdrop visuals and interactions.
- When provided, it replaces the default backdrop entirely (including default tap behavior)
- You are responsible for dismiss/collapse actions inside the custom component
- `backdropBehavior` still controls container-level pointer event behavior
```tsx
import { router } from "expo-router";
import { Pressable } from "react-native";
import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated";
import { useScreenAnimation } from "react-native-screen-transitions";
function SheetBackdrop() {
const animation = useScreenAnimation();
const style = useAnimatedStyle(() => ({
opacity: interpolate(animation.value.current.progress, [0, 1], [0, 0.4]),
backgroundColor: "#000",
}));
return (
router.back()}>
);
}
```
### Programmatic Control
Control snap points from anywhere in your app:
```tsx
import { snapTo } from "react-native-screen-transitions";
function BottomSheet() {
// Expand to full height (index 1)
const expand = () => snapTo(1);
// Collapse to half height (index 0)
const collapse = () => snapTo(0);
return (
);
}
```
The animated `snapIndex` is available in screen interpolators via `ScreenInterpolationProps`:
```tsx
screenStyleInterpolator: ({ snapIndex }) => {
// snapIndex interpolates between snap point indices
// e.g., 0.5 means halfway between snap point 0 and 1
return {
contentStyle: {
opacity: interpolate(snapIndex, [0, 1], [0.5, 1]),
},
};
}
```
### ScrollView Behavior
With `Transition.ScrollView` inside a snap-enabled sheet:
- **`expandViaScrollView: true`**: At boundary, swipe up expands and swipe down collapses (or dismisses at min if enabled)
- **`expandViaScrollView: false`**: Expand works only via deadspace; collapse/dismiss via scroll still works at boundary
- **Scrolled into content**: Normal scroll behavior
### Snap Animation Specs
Customize snap animations separately from enter/exit:
```tsx
transitionSpec: {
open: { stiffness: 1000, damping: 500, mass: 3 }, // Screen enter
close: { stiffness: 1000, damping: 500, mass: 3 }, // Screen exit
expand: { stiffness: 300, damping: 30 }, // Snap up
collapse: { stiffness: 300, damping: 30 }, // Snap down
}
```
---
## Shared Elements (Bounds API)
Animate elements between screens by tagging them.
### 1. Tag the Source
```tsx
navigation.navigate("Profile")}
>
```
### 2. Tag the Destination
```tsx
```
### 3. Use in Interpolator
```tsx
screenStyleInterpolator: ({ bounds }) => {
"worklet";
return {
avatar: bounds({ id: "avatar", method: "transform" }),
};
};
```
### Bounds Options
| Option | Values | Description |
| ----------- | ---------------------------------- | ----------------------------- |
| `id` | string | The `sharedBoundTag` to match |
| `method` | `"transform"` `"size"` `"content"` | How to animate |
| `space` | `"relative"` `"absolute"` | Coordinate space |
| `scaleMode` | `"match"` `"none"` `"uniform"` | Aspect ratio handling |
| `raw` | boolean | Return raw values |
---
## Overlays
Persistent UI that animates with the stack:
```tsx
const TabBar = ({ focusedIndex, progress }) => {
const style = useAnimatedStyle(() => ({
transform: [{ translateY: interpolate(progress.value, [0, 1], [100, 0]) }],
}));
return ;
};
```
### Overlay Props
| Prop | Description |
| -------------- | ------------------------------ |
| `focusedRoute` | Currently focused route |
| `focusedIndex` | Index of focused screen |
| `routes` | All routes in the stack |
| `progress` | Stack progress (derived value) |
| `navigation` | Navigation prop |
| `meta` | Custom metadata from options |
---
## Transition Components
| Component | Description |
| ----------------------- | -------------------------------------- |
| `Transition.View` | Animated view with `sharedBoundTag` |
| `Transition.Pressable` | Pressable that measures bounds |
| `Transition.ScrollView` | ScrollView with gesture coordination |
| `Transition.FlatList` | FlatList with gesture coordination |
| `Transition.MaskedView` | For reveal effects (requires native) |
---
## Hooks
### useScreenAnimation
Access animation state inside a screen:
```tsx
import { useScreenAnimation } from "react-native-screen-transitions";
function DetailScreen() {
const animation = useScreenAnimation();
const style = useAnimatedStyle(() => ({
opacity: animation.value.current.progress,
}));
return ...;
}
```
### useScreenState
Get navigation state without animation values:
```tsx
import { useScreenState } from "react-native-screen-transitions";
function DetailScreen() {
const { index, focusedRoute, routes, navigation } = useScreenState();
// ...
}
```
### useHistory
Access navigation history across the app:
```tsx
import { useHistory } from "react-native-screen-transitions";
function MyComponent() {
const { getRecent, getPath } = useHistory();
const recentScreens = getRecent(5); // Last 5 screens
const path = getPath(fromKey, toKey); // Path between screens
}
```
### useScreenGesture
Coordinate your own pan gestures with the navigation gesture:
```tsx
import { useScreenGesture } from "react-native-screen-transitions";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
function MyScreen() {
const screenGesture = useScreenGesture();
const myPanGesture = Gesture.Pan()
.simultaneousWithExternalGesture(screenGesture)
.onUpdate((e) => {
// Your gesture logic
});
return (
);
}
```
Use this when you have custom pan gestures that need to work alongside screen dismiss gestures.
---
## Advanced Animation Props
The full `screenStyleInterpolator` receives these props:
| Prop | Description |
| ---------------- | -------------------------------------------------------- |
| `progress` | Combined progress (0-2) |
| `stackProgress` | Accumulated progress across entire stack |
| `snapIndex` | Animated snap point index (-1 if no snap points) |
| `focused` | Whether this screen is the topmost in the stack |
| `current` | Current screen state |
| `previous` | Previous screen state |
| `next` | Next screen state |
| `active` | Screen driving the transition |
| `inactive` | Screen NOT driving the transition |
| `layouts.screen` | Screen dimensions |
| `insets` | Safe area insets |
| `bounds` | Shared element bounds function |
### Screen State Properties
Each screen state (`current`, `previous`, `next`, `active`, `inactive`) contains:
| Property | Description |
| ----------- | ---------------------------------------- |
| `progress` | Animation progress (0 or 1) |
| `closing` | Whether closing (0 or 1) |
| `entering` | Whether entering (0 or 1) |
| `animating` | Whether animating (0 or 1) |
| `gesture` | Gesture values (x, y, normalized values) |
| `meta` | Custom metadata from options |
### Using `meta` for Conditional Logic
Pass custom data between screens:
```tsx
// Screen A
options={{ meta: { hideTabBar: true } }}
// Screen B reads it
screenStyleInterpolator: (props) => {
"worklet";
const hideTabBar = props.inactive?.meta?.hideTabBar;
// ...
};
```
### Animate Individual Elements
Use `styleId` to target specific elements:
```tsx
// In options
screenStyleInterpolator: ({ progress }) => {
"worklet";
return {
"hero-image": {
opacity: interpolate(progress, [0, 1], [0, 1]),
},
};
};
// In component
```
---
## Stack Types
All three stacks share the same animation API. Choose based on your needs:
| Stack | Best For |
| ------------------- | --------------------------------------------------------- |
| **Blank Stack** | Most apps. Full control, all features. |
| **Native Stack** | When you need native screen primitives. |
| **Component Stack** | Embedded flows, isolated from React Navigation. *(Experimental)* |
### Blank Stack
The default choice. Uses `react-native-screens` for native screen containers, with animations powered by Reanimated worklets running on the UI thread (not the JS thread).
```tsx
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
```
### Native Stack
Extends `@react-navigation/native-stack`. Requires `enableTransitions: true`.
```tsx
import { createNativeStackNavigator } from "react-native-screen-transitions/native-stack";
```
### Component Stack (Experimental)
> **Note:** This API is experimental and may change based on community feedback.
Standalone navigator, not connected to React Navigation. Ideal for embedded flows.
```tsx
import { createComponentStackNavigator } from "react-native-screen-transitions/component-stack";
const Stack = createComponentStackNavigator();
```
---
## Caveats & Trade-offs
### Native Stack
The Native Stack uses transparent modal presentation to intercept transitions. This has trade-offs:
- **Delayed touch events** – Exiting screens may have briefly delayed touch response
- **beforeRemove listeners** – Relies on navigation lifecycle events
- **Rapid navigation** – Some edge cases with very fast navigation sequences
For most apps, Blank Stack avoids these issues entirely.
### Component Stack (Experimental)
- **No deep linking** – Routes aren't part of your URL structure
- **Isolated state** – Doesn't affect parent navigation
- **Touch pass-through** – Uses `pointerEvents="box-none"` by default
---
## Experimental Features
### High Refresh Rate
Force maximum refresh rate during transitions (for 90Hz/120Hz displays):
```tsx
options={{
experimental_enableHighRefreshRate: true,
}}
```
---
## Masked View Setup
Required for `SharedIGImage` and `SharedAppleMusic` presets. The masked view creates the "reveal" effect where content expands from the shared element.
> **Note**: Requires native code. Will not work in Expo Go.
### Installation
```bash
# Expo
npx expo install @react-native-masked-view/masked-view
# Bare React Native
npm install @react-native-masked-view/masked-view
cd ios && pod install
```
### Full Example
**1. Source Screen** – Tag pressable elements:
```tsx
// app/index.tsx
import { router } from "expo-router";
import { View } from "react-native";
import Transition from "react-native-screen-transitions";
export default function HomeScreen() {
return (
{
router.push({
pathname: "/details",
params: { sharedBoundTag: "album-art" },
});
}}
/>
);
}
```
**2. Destination Screen** – Wrap with MaskedView and match the tag:
```tsx
// app/details.tsx
import { useLocalSearchParams } from "expo-router";
import Transition from "react-native-screen-transitions";
export default function DetailsScreen() {
const { sharedBoundTag } = useLocalSearchParams<{ sharedBoundTag: string }>();
return (
{/* Additional screen content */}
);
}
```
**3. Layout** – Apply the preset with dynamic tag:
```tsx
// app/_layout.tsx
import Transition from "react-native-screen-transitions";
import { Stack } from "./stack";
export default function RootLayout() {
return (
({
...Transition.Presets.SharedAppleMusic({
sharedBoundTag: route.params?.sharedBoundTag ?? "",
}),
})}
/>
);
}
```
### How It Works
1. `Transition.Pressable` measures its bounds on press and stores them with the tag
2. `Transition.View` on the destination registers as the target for that tag
3. `Transition.MaskedView` clips content to the animating shared element bounds
4. The preset interpolates position, size, and mask for a seamless expand/collapse effect
---
## Support
This package is developed in my spare time.
If you'd like to fuel the next release, [buy me a coffee](https://buymeacoffee.com/trpfsu)
## License
MIT