Customizable screen transitions for React Native. Build gesture-driven, shared element, and fully custom animations with a simple API.
| iOS | Android |
|---|---|
ios.mp4 |
android.mp4 |
- Full Animation Control β Define exactly how screens enter, exit, and respond to gestures
- Bounds API + Navigation Zoom β Build shared element and fullscreen zoom transitions with one bounds helper
- Auto Snap Points β Use
snapPoints: ["auto"]and read measured content layout inside your interpolator - Gesture-Aware Scrollables β Transition-aware
ScrollViewandFlatListcoordinate with dismiss and snap gestures - Backdrop + Surface Slots β Animate screen content, backdrops, surfaces, and per-element slots from one interpolator
- Ready-Made Presets β Instagram, Apple Music, X (Twitter) style transitions included
3.4 introduces a newer, more explicit path for shared transitions and snap-driven layouts. Use the notes below as the source of truth when migrating examples or generating docs.
- Auto snap sizing with
snapPoints: ["auto"]andcurrent.layouts.content - Scoped screen state access through
previous,current,next,active, andinactive, withcurrent.layoutsandcurrent.snapIndex - Compound bounds components via
Transition.Boundary.View,Transition.Boundary.Trigger, andTransition.Boundary.Target Transition.createBoundaryComponentfor building custom boundary wrappers, includingalreadyAnimatedsupport- Navigation-style bounds zoom through
bounds({ id }).navigation.zoom() navigationMaskEnabledfor library-managed masked navigation transitions- Ancestor targeting in
useScreenGesture()anduseScreenAnimation() - Gesture release tuning with
gestureReleaseVelocityScaleandgestureReleaseVelocityMax Transition.Specs.FlingSpecfor underdamped fling-style release motion- Surface slot support through
surfaceComponentand the interpolatorsurfaceslot - Animated
propssupport across all slots via{ style, props }slot returns - Optional first-screen animation with
experimental_animateOnInitialMount logicallySettledfor choreography that should finish before a spring is fully at rest
sharedBoundTagon transition-aware components is deprecated for new work. PreferTransition.Boundary.*for new shared transition flows.Transition.MaskedViewis deprecated for new work. PreferTransition.Boundary.*withbounds({ id }).navigation.zoom()andnavigationMaskEnabledfor library-managed shared navigation transitions.createComponentStackNavigatoris deprecated. Prefer blank stack for embedded and independent flows.expandViaScrollViewwas renamed tosheetScrollGestureBehavior.- Flat interpolator keys are deprecated. Use
content,backdrop, andsurfaceinstead ofcontentStyle,backdropStyle, andoverlayStyle. - Legacy interpolator accessors are deprecated. Use
current.layoutsandcurrent.snapIndexinstead of top-levellayoutsandsnapIndex. - If you saw older alpha docs using
backgroundComponent/background, usesurfaceComponent/surface.
- Deprecated screen overlay mode and legacy overlay animation props were removed.
| 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.
npm install react-native-screen-transitionsnpm 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-contextimport { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import Transition from "react-native-screen-transitions";
const Stack = createBlankStackNavigator();
function App() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Detail"
component={DetailScreen}
options={{
...Transition.Presets.SlideFromBottom(),
}}
/>
</Stack.Navigator>
);
}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);Blank stack-specific navigator props follow React Navigation's custom navigator pattern.
For the dynamic API, pass them to <Stack.Navigator>:
const Stack = createBlankStackNavigator();
function App() {
return (
<Stack.Navigator independent enableNativeScreens={false}>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>
);
}For the static API, keep them in the same config object:
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
const Stack = createBlankStackNavigator({
initialRouteName: "Home",
screens: {
Home: HomeScreen,
Detail: DetailScreen,
},
independent: true,
enableNativeScreens: false,
});Use built-in presets for common transitions:
<Stack.Screen
name="Detail"
options={{
...Transition.Presets.SlideFromBottom(),
}}
/>| 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 }) |
Legacy Instagram-style shared image preset |
SharedAppleMusic({ sharedBoundTag }) |
Legacy Apple Music-style shared element preset |
SharedXImage({ sharedBoundTag }) |
Legacy X (Twitter)-style image transition preset |
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)
options={{
screenStyleInterpolator: ({ progress }) => {
"worklet";
return {
content: {
style: {
opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
},
},
};
},
}}options={{
screenStyleInterpolator: ({ progress, current: { layouts: { screen } } }) => {
"worklet";
return {
content: {
style: {
transform: [{
translateX: interpolate(
progress,
[0, 1, 2],
[screen.width, 0, -screen.width * 0.3]
),
}],
},
},
};
},
}}options={{
screenStyleInterpolator: ({ progress, current: { layouts: { screen } } }) => {
"worklet";
return {
content: {
style: {
transform: [{
translateY: interpolate(progress, [0, 1], [screen.height, 0]),
}],
},
},
};
},
}}Your interpolator can return:
return {
content: { style: { ... }, props: { ... } }, // Main screen slot
backdrop: { style: { ... }, props: { ... } }, // Backdrop / blur / dimming
surface: { style: { ... }, props: { ... } }, // Custom surface layer
["my-id"]: { style: { ... }, props: { ... } }, // Specific element via styleId
};Every slot supports animated style and animated props.
Use the shorthand style-only form when you only need styles, or the explicit
{ style, props } form when the wrapped component exposes animatable props.
Return null, undefined, or {} when you want an interpolator frame to apply no transition styles:
screenStyleInterpolator: ({ bounds }) => {
"worklet";
const snapshot = bounds.getSnapshot("hero");
if (!snapshot) return null;
return {
content: {
style: { opacity: 1 },
},
};
};Control timing with spring configs:
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
},
}}Built-in starting points are available on Transition.Specs:
transitionSpec: {
open: Transition.Specs.DefaultSpec,
close: Transition.Specs.FlingSpec,
expand: Transition.Specs.DefaultSnapSpec,
collapse: Transition.Specs.DefaultSnapSpec,
}Enable swipe-to-dismiss:
options={{
gestureEnabled: true,
gestureDirection: "vertical",
...Transition.Presets.SlideFromBottom(),
}}| 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) |
gestureReleaseVelocityScale |
Multiplier for release velocity used by post-release spring animations |
gestureReleaseVelocityMax |
Max absolute normalized release velocity used by spring animations |
sheetScrollGestureBehavior |
Nested scroll handoff mode: "expand-and-collapse" or "collapse-only" |
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) |
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"]// 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",
}Use transition-aware scrollables so gestures work correctly:
<Transition.ScrollView>
{/* content */}
</Transition.ScrollView>
<Transition.FlatList data={items} renderItem={...} />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
Create multi-stop sheets that snap to defined positions. Works with any gesture direction (bottom sheets, top sheets, side sheets), and supports intrinsic content sizing with "auto" snap points.
// Bottom sheet (most common)
<Stack.Screen
name="Sheet"
options={{
gestureEnabled: true,
gestureDirection: "vertical",
snapPoints: [0.5, 1], // 50% and 100% of screen
initialSnapIndex: 0, // Start at 50%
backdropBehavior: "dismiss", // Tap backdrop to dismiss
...Transition.Presets.SlideFromBottom(),
}}
/>
// Side sheet (same API, different direction)
<Stack.Screen
name="SidePanel"
options={{
gestureEnabled: true,
gestureDirection: "horizontal",
snapPoints: [0.3, 0.7, 1], // 30%, 70%, 100% of screen width
initialSnapIndex: 1,
// Add a horizontal screenStyleInterpolator for drawer-style motion
}}
/>
// Auto-sized sheet
<Stack.Screen
name="Composer"
options={{
gestureEnabled: true,
gestureDirection: "vertical",
snapPoints: ["auto", 1],
initialSnapIndex: 0,
backdropBehavior: "collapse",
...Transition.Presets.SlideFromBottom(),
}}
/>| Option | Description |
|---|---|
snapPoints |
Array of fractions (0-1) or "auto" values where sheet can rest |
initialSnapIndex |
Index of initial snap point (default: 0) |
gestureSnapLocked |
Locks gesture snapping to current point (programmatic snapTo still works) |
sheetScrollGestureBehavior |
Nested scroll handoff mode for snap sheets |
backdropBehavior |
Touch handling: "block", "passthrough", "dismiss", "collapse" |
backdropComponent |
Custom backdrop component; replaces default backdrop + tap handling |
When you use "auto", the library measures the content height and exposes it in current.layouts.content:
screenStyleInterpolator: ({ current }) => {
"worklet";
const contentHeight = current.layouts.content?.height ?? 0;
return {
content: {
style: {
opacity: interpolate(current.snapIndex, [0, 1], [0.8, 1]),
},
},
"sheet-height-debug": {
style: {
opacity: contentHeight > 0 ? 1 : 0,
},
},
};
}| 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 |
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
backdropBehaviorstill controls container-level pointer event behavior
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 (
<Pressable style={{ flex: 1 }} onPress={() => router.back()}>
<Animated.View style={[{ flex: 1 }, style]} />
</Pressable>
);
}
<Stack.Screen
name="Sheet"
options={{
snapPoints: [0.5, 1],
backdropBehavior: "dismiss",
backdropComponent: SheetBackdrop,
}}
/>Control snap points from anywhere in your app:
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 (
<View>
<Button title="Expand" onPress={expand} />
<Button title="Collapse" onPress={collapse} />
</View>
);
}The animated snap point index is available via current.snapIndex in ScreenInterpolationProps:
screenStyleInterpolator: ({ current }) => {
// snapIndex interpolates between snap point indices
// e.g., 0.5 means halfway between snap point 0 and 1
return {
content: {
style: {
opacity: interpolate(current.snapIndex, [0, 1], [0.5, 1]),
},
},
};
}With Transition.ScrollView inside a snap-enabled sheet:
sheetScrollGestureBehavior: "expand-and-collapse": At boundary, swipe up expands and swipe down collapses (or dismisses at min if enabled)sheetScrollGestureBehavior: "collapse-only": Expand works only via deadspace; collapse/dismiss via scroll still works at boundary- Scrolled into content: Normal scroll behavior
Customize snap animations separately from enter/exit:
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
}Animate elements between screens by tagging them. In 3.4, the recommended and forward-compatible API is Transition.Boundary.* for explicit bounds ownership.
sharedBoundTag on transition-aware components is deprecated and retained for legacy flows and presets.
<Transition.Boundary.Trigger
id="avatar"
onPress={() => navigation.navigate("Profile")}
>
<Image source={avatar} style={{ width: 50, height: 50 }} />
</Transition.Boundary.Trigger>If the boundary owner is larger than the visual element you want to match,
wrap the measured descendant in Transition.Boundary.Target:
<Transition.Boundary.Trigger
id="avatar"
onPress={() => navigation.navigate("Profile")}
style={styles.card}
>
<Transition.Boundary.Target>
<Image source={avatar} style={styles.image} />
</Transition.Boundary.Target>
<View style={styles.copy}>
<Text style={styles.title}>Profile</Text>
</View>
</Transition.Boundary.Trigger><Transition.Boundary.View id="avatar">
<Image source={avatar} style={{ width: 200, height: 200 }} />
</Transition.Boundary.View>screenStyleInterpolator: ({ bounds }) => {
"worklet";
return {
avatar: bounds({ id: "avatar", method: "transform" }),
};
};For fullscreen, navigation-style shared transitions:
screenStyleInterpolator: ({ bounds }) => {
"worklet";
return bounds({ id: "avatar" }).navigation.zoom({
target: "fullscreen",
});
};| Option | Values | Description |
|---|---|---|
id |
string | The boundary id to match |
group |
string | Optional group key for paged/detail flows |
method |
"transform" "size" "content" |
How to animate |
space |
"relative" "absolute" |
Coordinate space |
scaleMode |
"match" "none" "uniform" |
Aspect ratio handling |
raw |
boolean | Return raw values |
Persistent UI that animates with the stack:
const TabBar = ({ focusedIndex, progress }) => {
const style = useAnimatedStyle(() => ({
transform: [{ translateY: interpolate(progress.value, [0, 1], [100, 0]) }],
}));
return <Animated.View style={[styles.tabBar, style]} />;
};
<Stack.Screen
name="Home"
options={{
overlay: TabBar,
overlayShown: true,
}}
/>| 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 |
| Component | Description |
|---|---|
Transition.View |
Animated view; sharedBoundTag usage is legacy/deprecated |
Transition.Pressable |
Pressable; sharedBoundTag usage is legacy/deprecated |
Transition.ScrollView |
ScrollView with gesture coordination |
Transition.FlatList |
FlatList with gesture coordination |
Transition.Boundary.View |
Explicit bounds destination/source registration |
Transition.Boundary.Trigger |
Pressable boundary owner that captures source bounds on press |
Transition.Boundary.Target |
Optional nested measurement target inside a boundary owner |
Transition.MaskedView |
Deprecated legacy reveal helper (requires native) |
Helper exports:
Transition.createBoundaryComponent(Component, { alreadyAnimated?: boolean })Transition.createTransitionAwareComponent(Component, { isScrollable?: boolean, alreadyAnimated?: boolean })
Access animation state inside a screen:
import { useScreenAnimation } from "react-native-screen-transitions";
function DetailScreen() {
const animation = useScreenAnimation();
const parentAnimation = useScreenAnimation("parent");
const style = useAnimatedStyle(() => ({
opacity: parentAnimation.value.current.progress,
}));
return <Animated.View style={style}>...</Animated.View>;
}Get navigation state without animation values:
import { useScreenState } from "react-native-screen-transitions";
function DetailScreen() {
const { index, focusedRoute, routes, navigation } = useScreenState();
// ...
}Access navigation history across the app:
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
}Coordinate your own pan gestures with the navigation gesture:
import { useScreenGesture } from "react-native-screen-transitions";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
function MyScreen() {
const screenGesture = useScreenGesture();
const parentGesture = useScreenGesture("parent");
const myPanGesture = Gesture.Pan()
.simultaneousWithExternalGesture(screenGesture, parentGesture)
.onUpdate((e) => {
// Your gesture logic
});
return (
<GestureDetector gesture={myPanGesture}>
<View />
</GestureDetector>
);
}Use this when you have custom pan gestures that need to work alongside screen dismiss gestures.
You can target "self", "parent", "root", or { ancestor: number }.
The full screenStyleInterpolator receives these props:
| Prop | Description |
|---|---|
progress |
Combined progress (0-2) |
stackProgress |
Accumulated progress across entire stack |
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 |
insets |
Safe area insets |
bounds |
Shared element bounds function |
Prefer current.snapIndex, current.layouts.screen, and current.layouts.content for new code.
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) |
logicallySettled |
Whether choreography can treat the screen as done |
snapIndex |
Animated snap point index for this screen |
layouts |
Screen and measured content layouts |
gesture |
Gesture values (x, y, normX, normY, etc.) |
meta |
Custom metadata from options |
Pass custom data between screens:
// Screen A
options={{ meta: { hideTabBar: true } }}
// Screen B reads it
screenStyleInterpolator: (props) => {
"worklet";
const hideTabBar = props.inactive?.meta?.hideTabBar;
// ...
};Use styleId to target specific elements:
// In options
screenStyleInterpolator: ({ progress }) => {
"worklet";
return {
"hero-image": {
style: {
opacity: interpolate(progress, [0, 1], [0, 1]),
},
},
};
};
// In component
<Transition.View styleId="hero-image">
<Image source={...} />
</Transition.View>Blank stack and native stack are the primary APIs. Component stack remains available as a deprecated compatibility API.
| Stack | Best For |
|---|---|
| Blank Stack | Most apps. Full control, all features. |
| Native Stack | When you need native screen primitives. |
| Component Stack | Legacy embedded-flow API. Prefer blank stack instead. |
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).
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";Extends @react-navigation/native-stack. Requires enableTransitions: true.
import { createNativeStackNavigator } from "react-native-screen-transitions/native-stack";
<Stack.Screen
name="Detail"
options={{
enableTransitions: true,
...Transition.Presets.SlideFromBottom(),
}}
/>Note: Prefer blank stack for new work. It now covers the embedded and independent flow use case.
Standalone navigator, not connected to React Navigation. Kept for compatibility with older integrations.
import { createComponentStackNavigator } from "react-native-screen-transitions/component-stack";
const Stack = createComponentStackNavigator();
<Stack.Navigator initialRouteName="step1">
<Stack.Screen name="step1" component={Step1} />
<Stack.Screen name="step2" component={Step2} />
</Stack.Navigator>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.
- 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
Force maximum refresh rate during transitions (for 90Hz/120Hz displays):
options={{
experimental_enableHighRefreshRate: true,
}}Animate the first screen in a navigator instead of snapping it directly to the settled state:
options={{
experimental_animateOnInitialMount: true,
}}Transition.MaskedView and sharedBoundTag are deprecated for new work.
Prefer Transition.Boundary.* for explicit shared-element ownership, and prefer bounds({ id }).navigation.zoom() with navigationMaskEnabled when you want library-managed navigation-style masked transitions.
If you used early 3.4 prerelease docs or examples, maskEnabled remains accepted as a compatibility alias, but new docs and examples should use navigationMaskEnabled.
This section is kept for compatibility with legacy presets such as SharedIGImage and SharedAppleMusic.
Note: Requires native code. Will not work in Expo Go.
# Expo
npx expo install @react-native-masked-view/masked-view
# Bare React Native
npm install @react-native-masked-view/masked-view
cd ios && pod install1. Source Screen β Tag pressable elements:
// 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 (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Transition.Pressable
sharedBoundTag="album-art"
style={{
width: 200,
height: 200,
backgroundColor: "#1DB954",
borderRadius: 12,
}}
onPress={() => {
router.push({
pathname: "/details",
params: { sharedBoundTag: "album-art" },
});
}}
/>
</View>
);
}2. Destination Screen β Wrap with MaskedView and match the tag:
// 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 (
<Transition.MaskedView style={{ flex: 1, backgroundColor: "#121212" }}>
<Transition.View
sharedBoundTag={sharedBoundTag}
style={{
backgroundColor: "#1DB954",
width: 400,
height: 400,
alignSelf: "center",
borderRadius: 12,
}}
/>
{/* Additional screen content */}
</Transition.MaskedView>
);
}3. Layout β Apply the preset with dynamic tag:
// 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="details"
options={({ route }) => ({
...Transition.Presets.SharedAppleMusic({
sharedBoundTag: route.params?.sharedBoundTag ?? "",
}),
})}
/>
</Stack>
);
}Transition.Pressablemeasures its bounds on press and stores them with the tagTransition.Viewon the destination registers as the target for that tagTransition.MaskedViewclips content to the animating shared element bounds- The preset interpolates position, size, and mask for a seamless expand/collapse effect
This package is developed in my spare time.
If you'd like to fuel the next release, buy me a coffee
MIT