The SharedAppleMusic preset creates an Apple Music-style shared element transition where an element expands from a small card to fullscreen with dynamic shadows and gesture-driven dismissal.
This preset requires @react-native-masked-view/masked-view. See Masked View Setup for installation.
Function Signature
SharedAppleMusic (
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, platform shadows, and directional drag Enables custom transitions
Enables drag-to-dismiss gesture
gestureDirection
array
default: "[\"vertical\", \"horizontal\"]"
Allows dismissal by dragging in any direction
Gesture affects dismissal but doesn’t directly drive progress value
Complex interpolator with platform-specific shadows and directional resistance
Asymmetric spring configs for natural open/close feel
Implementation Details
Transition Spec
Asymmetric spring configuration for natural feeling:
transitionSpec : {
open : {
stiffness : 1000 ,
damping : 500 ,
mass : 3 ,
overshootClamping : true ,
restSpeedThreshold : 0.02 ,
},
close : {
stiffness : 600 ,
damping : 60 ,
mass : 4 ,
overshootClamping : false ,
restDisplacementThreshold : 0.002 ,
},
}
Key differences:
Open : Snappy, tight spring (high damping) with clamping
Close : Looser, bouncier spring (lower damping) without clamping for natural settle
Screen Style Interpolator
The interpolator features directional drag resistance and platform-specific shadows.
Directional Drag Resistance
const initialDirection = active . gesture . direction ;
const xResistance = initialDirection === "horizontal" ? 0.7 : 0.4 ;
const yResistance = initialDirection === "vertical" ? 0.7 : 0.4 ;
const xScaleOuput = initialDirection === "horizontal" ? [ 1 , 0.5 ] : [ 1 , 1 ];
const yScaleOuput = initialDirection === "vertical" ? [ 1 , 0.5 ] : [ 1 , 1 ];
const dragX = interpolate (
normX ,
[ - 1 , 0 , 1 ],
[ - screen . width * xResistance , 0 , screen . width * xResistance ],
"clamp" ,
);
const dragY = interpolate (
normY ,
[ - 1 , 0 , 1 ],
[ - screen . height * yResistance , 0 , screen . height * yResistance ],
"clamp" ,
);
Behavior:
Primary drag direction has less resistance (70%)
Secondary direction has more resistance (40%)
Scale reduction only applies to primary direction
const dragMagnitude = Math . max ( Math . abs ( normX ), Math . abs ( normY ));
const shadowOpacity = interpolate ( dragMagnitude , [ 0 , 1 ], [ 0 , 0.25 ], "clamp" );
const shadowRadius = interpolate ( dragMagnitude , [ 0 , 1 ], [ 0 , 24 ], "clamp" );
const shadowOffsetY = interpolate ( dragMagnitude , [ 0 , 1 ], [ 0 , 20 ], "clamp" );
const elevation = interpolate ( dragMagnitude , [ 0 , 1 ], [ 0 , 24 ], "clamp" );
const IOSShadowStyle = {
shadowColor: "#000" ,
shadowOpacity ,
shadowRadius ,
shadowOffset: { width: 0 , height: shadowOffsetY },
};
const AndroidShadowStyle = {
elevation ,
shadowColor: "#000" ,
};
Shadow behavior:
Shadows increase with drag magnitude
iOS: Uses shadowOpacity, shadowRadius, shadowOffset
Android: Uses elevation
Creates depth as card lifts away from screen
Focused Screen (Destination)
if ( focused ) {
return {
contentStyle: {
pointerEvents: current . animating ? "none" : "auto" ,
transform: [
{ translateX: dragX || 0 },
{ translateY: dragY || 0 },
{ scale: dragXScale },
{ scale: dragYScale },
],
opacity ,
... ( platform === "ios" ? IOSShadowStyle : AndroidShadowStyle ),
},
_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 ]),
},
};
}
Unfocused Screen (Source)
const scaledBoundTranslateX = ( boundValues . translateX || 0 ) * dragXScale ;
const scaledBoundTranslateY = ( boundValues . translateY || 0 ) * dragYScale ;
const scaledBoundScaleX = ( boundValues . scaleX || 1 ) * dragXScale ;
const scaledBoundScaleY = ( boundValues . scaleY || 1 ) * dragYScale ;
const contentScale = interpolate ( progress , [ 1 , 2 ], [ 1 , 0.9 ], "clamp" );
return {
[sharedBoundTag]: {
transform: [
{ translateX: dragX || 0 },
{ translateY: dragY || 0 },
{ translateX: scaledBoundTranslateX },
{ translateY: scaledBoundTranslateY },
{ scale: dragXScale },
{ scale: dragYScale },
{ scaleX: scaledBoundScaleX },
{ scaleY: scaledBoundScaleY },
],
opacity ,
zIndex: current . animating ? 999 : - 1 ,
position: "relative" ,
},
contentStyle: {
transform: [{ scale: contentScale }],
},
};
Unfocused features:
Background screen scales down to 90% when transitioning away
Bound element combines drag transform with bound transform
Z-index elevated during animation
Opacity fades based on progress
Usage Example
Source Screen
Destination Screen
Layout Configuration
Custom Shadow Color
// app/library.tsx
import { router } from "expo-router" ;
import Transition from "react-native-screen-transitions" ;
export default function LibraryScreen () {
return (
< Transition.Pressable
sharedBoundTag = "album-art"
style = { {
width: 150 ,
height: 150 ,
backgroundColor: "#1DB954" ,
borderRadius: 8 ,
} }
onPress = { () => {
router . push ({
pathname: "/now-playing" ,
params: { sharedBoundTag: "album-art" },
});
} }
>
< Image
source = { { uri: "https://example.com/album.jpg" } }
style = { { width: "100%" , height: "100%" , borderRadius: 8 } }
/>
</ Transition.Pressable >
);
}
Key Features
Directional resistance - More freedom in primary drag direction
Dynamic shadows - Shadows increase as you drag (platform-specific)
Asymmetric springs - Snappy open, bouncy close
Scaled background - Previous screen scales down slightly
Top anchor - Bounds use top anchor for natural card expansion
Masked reveal - Content clips to animating bounds
Gesture Behavior
The preset adapts to your gesture direction:
// If you start dragging horizontally:
// - 70% resistance (can drag 70% of screen width)
// - Scale reduces horizontally
// If you start dragging vertically:
// - 70% resistance (can drag 70% of screen height)
// - Scale reduces vertically
// Secondary direction always has 40% resistance
Notes
The shadow effect is platform-specific . iOS uses proper shadow properties while Android uses elevation. Both create similar visual effects.
The close animation has overshootClamping: false, allowing the element to bounce slightly as it settles back into place, matching Apple Music’s natural feel.
Requires Transition.MaskedView to wrap the destination screen. Without it, content will render incorrectly during bounds transformation.