The SharedIGImage preset creates an Instagram-style shared element transition where an image expands from a thumbnail to fullscreen with gesture-driven dismissal.
This preset requires @react-native-masked-view/masked-view. See Masked View Setup for installation.
Function Signature
SharedIGImage (
config : Partial < ScreenTransitionConfig > & {
sharedBoundTag: string ;
}
): ScreenTransitionConfig
Parameters
Configuration object with required sharedBoundTag The unique identifier that links the source and destination elements. Must match the sharedBoundTag on both Transition.Pressable (source) and Transition.View (destination).
...other
Partial<ScreenTransitionConfig>
Any other screen transition config properties to override
Returns
Configuration with shared element bounds animation and gesture handling Enables drag-to-dismiss gesture
gestureDirection
array
default: "[\"vertical\", \"horizontal\"]"
Allows dismissal by dragging in any direction
Enables custom transitions
Gesture affects dismissal but doesn’t directly drive progress value
Complex interpolator handling bounds transformation, masking, and drag
Custom spring configuration for smooth shared element animation
Implementation Details
Transition Spec
Custom spring configuration for smooth shared element morphing:
transitionSpec : {
open : {
stiffness : 1500 ,
damping : 1000 ,
mass : 3 ,
overshootClamping : true ,
restSpeedThreshold : 0.02 ,
},
close : {
stiffness : 1500 ,
damping : 1000 ,
mass : 3 ,
overshootClamping : true ,
restSpeedThreshold : 0.02 ,
},
}
Higher stiffness and damping create snappier, more responsive shared element animations.
Screen Style Interpolator
The interpolator handles two separate states:
1. Focused Screen (Destination)
When the screen is focused (fullscreen image view):
if ( focused ) {
const boundValues = bounds ({
id: sharedBoundTag ,
method: "content" ,
scaleMode: "uniform" ,
raw: true ,
});
const maskedValues = bounds ({
id: sharedBoundTag ,
space: "absolute" ,
target: "fullscreen" ,
method: "size" ,
raw: true ,
});
return {
backdropStyle: {
backgroundColor: "black" ,
opacity: interpolate ( progress , [ 0 , 1 ], [ 0 , 0.5 ]),
},
contentStyle: {
transform: [
{ translateX: dragX },
{ translateY: dragY },
{ scale: dragXScale },
{ scale: dragYScale },
],
},
_ROOT_CONTAINER: {
transform: [
{ translateX: boundValues . translateX || 0 },
{ translateY: boundValues . translateY || 0 },
{ scale: boundValues . scale || 1 },
],
},
_ROOT_MASKED: {
width: maskedValues . width ,
height: maskedValues . height ,
transform: [
{ translateX: maskedValues . translateX || 0 },
{ translateY: maskedValues . translateY || 0 },
],
borderRadius: interpolate ( progress , [ 0 , 1 ], [ 0 , 24 ]),
},
};
}
2. Unfocused Screen (Source)
When the screen is not focused (thumbnail in list):
return {
contentStyle: {
pointerEvents: current . gesture . isDismissing ? "none" : "auto" ,
},
[sharedBoundTag]: {
transform: [
{ translateX: dragX || 0 },
{ translateY: dragY || 0 },
{ translateX: boundValues . translateX || 0 },
{ translateY: boundValues . translateY || 0 },
{ scaleX: boundValues . scaleX || 1 },
{ scaleY: boundValues . scaleY || 1 },
{ scale: dragXScale },
{ scale: dragYScale },
],
},
};
Drag animations:
const dragX = interpolate (
normX ,
[ - 1 , 0 , 1 ],
[ - width * 0.7 , 0 , width * 0.7 ],
"clamp" ,
);
const dragY = interpolate (
normY ,
[ - 1 , 0 , 1 ],
[ - height * 0.4 , 0 , height * 0.4 ],
"clamp" ,
);
const dragXScale = interpolate ( normX , [ 0 , 1 ], [ 1 , 0.8 ]);
const dragYScale = interpolate ( normY , [ 0 , 1 ], [ 1 , 0.8 ]);
Usage Example
Source Screen
Destination Screen
Layout Configuration
Custom Backdrop
// app/feed.tsx
import { router } from "expo-router" ;
import Transition from "react-native-screen-transitions" ;
export default function FeedScreen () {
return (
< Transition.Pressable
sharedBoundTag = "photo-1"
onPress = { () => {
router . push ({
pathname: "/photo" ,
params: { sharedBoundTag: "photo-1" },
});
} }
>
< Image
source = { { uri: "https://example.com/photo.jpg" } }
style = { { width: 200 , height: 200 , borderRadius: 12 } }
/>
</ Transition.Pressable >
);
}
How It Works
Source tagging : Transition.Pressable with sharedBoundTag measures bounds on press
Destination tagging : Transition.View with matching tag registers as animation target
Masking : Transition.MaskedView clips content to animated bounds
Bounds transformation : Interpolates position, size, and scale between source and destination
Gesture handling : Drag applies translation and scale while maintaining bound animation
Key Features
Seamless morphing - Element smoothly expands from thumbnail to fullscreen
Backdrop fade - Black backdrop fades in as image expands
Drag to dismiss - Drag in any direction with scale feedback
Border radius animation - Corners animate from original radius to 24px
Masked reveal - Content outside the image bounds is clipped during transition
Notes
The sharedBoundTag must be unique within the navigation flow and must match exactly between source and destination.
Use route params to pass the sharedBoundTag dynamically when navigating to different images.
Requires Transition.MaskedView to wrap the destination screen for proper clipping. Without it, content will render incorrectly during the transition.