Skip to main content
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

config
object
required
Configuration object with required sharedBoundTag

Returns

ScreenTransitionConfig
object
Configuration with shared element bounds, platform shadows, and directional drag

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

Platform-Specific Shadows

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

// 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.