Skip to main content

The Bounds API

The bounds API measures layout coordinates of elements and allows you to animate them across screens. This creates smooth “shared element” transitions like morphing an avatar into a header image.

Using sharedBoundTag

Tag elements on both source and destination screens:
// Source screen (HomeScreen)
import Transition from "react-native-screen-transitions";

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Transition.Pressable
        sharedBoundTag="avatar"
        onPress={() => navigation.navigate("Profile")}
        style={{ width: 60, height: 60 }}
      >
        <Image
          source={{ uri: "https://..." }}
          style={{ width: 60, height: 60, borderRadius: 30 }}
        />
      </Transition.Pressable>
    </View>
  );
}

// Destination screen (ProfileScreen)
function ProfileScreen() {
  return (
    <View style={{ flex: 1 }}>
      <Transition.View
        sharedBoundTag="avatar"
        style={{ width: 120, height: 120 }}
      >
        <Image
          source={{ uri: "https://..." }}
          style={{ width: 120, height: 120, borderRadius: 60 }}
        />
      </Transition.View>
    </View>
  );
}
Use Transition.Pressable on the source (interactive) and Transition.View on the destination.

Using bounds() in Interpolators

Access bounds measurements in your interpolator using the bounds() function:
import Transition from "react-native-screen-transitions";
import { interpolate } from "react-native-reanimated";

const profileOptions = {
  screenStyleInterpolator: ({ progress, bounds }) => {
    "worklet";

    // Get the transform to animate from source to destination layout
    const avatar = bounds({
      id: "avatar",
      method: "transform",
    });

    return {
      contentStyle: {
        opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
      },
      // Apply bounds transform to the avatar element
      "avatar-wrapper": avatar,
    };
  },
};

<Stack.Screen
  name="Profile"
  component={ProfileScreen}
  options={profileOptions}
/>
The bounds() function returns style transforms that morph the destination element to match the source, then animate back.

bounds() Options

OptionTypeDescription
idstringElement ID (must match sharedBoundTag)
method”transform” | “size” | “content”How to animate the element
space”relative” | “absolute”Coordinate space
scaleMode”match” | “none” | “uniform”Scale behavior
targetstringTarget element (for nested layouts)
anchorAnchor point for transforms
rawbooleanReturn raw bounds object

bounds() Methods

  • “transform” — Animate position and scale using transforms (recommended for performance)
  • “size” — Animate width and height directly
  • “content” — Animate the entire layout box

Bounds Utilities

Use bounds utility functions for advanced layouts:

bounds.getSnapshot(id)

Get current measurements of an element:
screenStyleInterpolator: ({ bounds }) => {
  "worklet";
  const snapshot = bounds.getSnapshot("avatar");
  // snapshot = { x, y, width, height, scale, ... }
};
Get a link object for two-way binding:
const link = bounds.getLink("avatar");
// Use in multiple places

bounds.interpolateStyle(id, …)

Interpolate bounds-based styles:
screenStyleInterpolator: ({ progress, bounds }) => {
  "worklet";
  return {
    "avatar": bounds.interpolateStyle(
      "avatar",
      [0, 1, 2],
      [
        { opacity: 0, scale: 0.5 },
        { opacity: 1, scale: 1 },
        { opacity: 0, scale: 0.5 },
      ]
    ),
  };
};

bounds.interpolateBounds(id, …)

Interpolate between different bound states:
const style = bounds.interpolateBounds(
  "avatar",
  progress,
  [
    { x: 0, y: 0, width: 60, height: 60 },
    { x: 50, y: 100, width: 200, height: 200 },
  ]
);

Full Example: Image Transition

A complete example animating an image across screens:
import Transition from "react-native-screen-transitions";
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import { View, Text, Image, TouchableOpacity } from "react-native";
import Animated, { interpolate } from "react-native-reanimated";

const Stack = createBlankStackNavigator();

// Home screen with tap target
function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, padding: 20, justifyContent: "center" }}>
      <TouchableOpacity
        onPress={() => navigation.navigate("ImageDetail")}
        style={{ alignSelf: "center" }}
      >
        <Transition.Pressable
          sharedBoundTag="photo"
          style={{ width: 100, height: 100, marginBottom: 10 }}
        >
          <Image
            source={{ uri: "https://picsum.photos/100" }}
            style={{ width: 100, height: 100, borderRadius: 8 }}
          />
        </Transition.Pressable>
      </TouchableOpacity>
      <Text style={{ textAlign: "center" }}>Tap image to expand</Text>
    </View>
  );
}

// Detail screen with full image
function ImageDetailScreen() {
  return (
    <View style={{ flex: 1, backgroundColor: "#000" }}>
      <Transition.View
        sharedBoundTag="photo"
        style={{ width: "100%", height: "50%", alignSelf: "center" }}
      >
        <Image
          source={{ uri: "https://picsum.photos/500" }}
          style={{ width: "100%", height: "100%" }}
        />
      </Transition.View>
    </View>
  );
}

export default function App() {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen
        name="ImageDetail"
        component={ImageDetailScreen}
        options={{
          screenStyleInterpolator: ({ progress, bounds }) => {
            "worklet";

            const photoTransform = bounds({
              id: "photo",
              method: "transform",
            });

            return {
              contentStyle: {
                backgroundColor: interpolate(
                  progress,
                  [0, 1, 2],
                  ["transparent", "#000", "#000"]
                ),
              },
              "photo": photoTransform,
            };
          },
          gestureEnabled: true,
          gestureDirection: "vertical",
        }}
      />
    </Stack.Navigator>
  );
}

Presets with Shared Elements

The library includes ready-made presets for complex shared element animations:

SharedIGImage

Instagram-style image expansion:
options={{
  ...Transition.Presets.SharedIGImage(),
}}
Requires @react-native-masked-view/masked-view and native setup.

SharedAppleMusic

Apple Music-style album to player transition with masked reveals:
options={{
  ...Transition.Presets.SharedAppleMusic(),
}}
Also requires @react-native-masked-view/masked-view.

SharedXImage

X/Twitter-style image expansion:
options={{
  ...Transition.Presets.SharedXImage(),
}}
See Masked View Setup for native configuration.

Performance Optimization

When animating many elements:
  1. Use method: "transform" instead of "size" for better performance
  2. Use scaleMode: "uniform" to avoid aspect ratio distortion
  3. Keep bounds measurements simple — avoid deeply nested hierarchies
  4. Consider using backdropComponent to control rendering

Next Steps