Skip to main content

Shared Element Overview

Shared elements create visual continuity by animating a component from one screen to another. The source element bounds are measured, then animated to match the destination element’s position and size.
// Source (Home screen)
<Transition.Boundary.Pressable id="avatar" onPress={navigate}>
  <Image source={{ uri: 'https://...' }} style={{ width: 80, height: 80 }} />
</Transition.Boundary.Pressable>

// Destination (Details screen)
<Transition.Boundary.View id="avatar">
  <Image source={{ uri: 'https://...' }} style={{ width: 200, height: 200 }} />
</Transition.Boundary.View>
The animation library measures both positions and automatically interpolates between them.

Boundary Components

Use explicit boundary components to define shared element source and destination:

Transition.Boundary.View

An animated view that can serve as a shared element destination:
<Transition.Boundary.View
  id="hero"
  style={{ backgroundColor: 'blue', width: 100, height: 100 }}
>
  <Text>Destination</Text>
</Transition.Boundary.View>

Transition.Boundary.Pressable

A pressable boundary that captures source bounds when tapped:
<Transition.Boundary.Pressable
  id="card"
  onPress={() => navigation.navigate('Details')}
>
  <View style={{ width: 100, height: 100, backgroundColor: 'red' }}>
    <Text>Tap to navigate</Text>
  </View>
</Transition.Boundary.Pressable>
When pressed, the library captures this element’s bounds as the animation source.

Transition.Boundary.Target

Register arbitrary elements as animation targets:
<Transition.Boundary.Target id="special">
  <CustomComponent />
</Transition.Boundary.Target>

Accessing Bounds in Interpolators

Use the bounds() function in your screenStyleInterpolator to query shared element data:
const screenStyleInterpolator = ({ progress, bounds }) => {
  "worklet";

  const avatarBounds = bounds({ id: "avatar" });

  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
  };
};

Bounds Result

The bounds() function returns an object with:
PropertyTypeDescription
xnumberHorizontal position
ynumberVertical position
widthnumberElement width
heightnumberElement height
scalenumberScale factor between source and destination
transformobjectTransform matrices (translate, scale)

Full Bounds Example

const sharedElementInterpolator = ({ progress, bounds }) => {
  "worklet";

  const b = bounds({ id: "hero" });

  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
    "hero": {
      style: {
        // Animate the shared element's properties
        borderRadius: interpolate(progress, [0, 1], [12, 0]),
        backgroundColor: interpolate(
          progress,
          [0, 1],
          ["#FF6B6B", "#4ECDC4"]
        ),
      },
    },
  };
};
Animate a shared element from its source to fullscreen with optimized transforms:
const screenStyleInterpolator = ({ bounds }) => {
  "worklet";
  return bounds({ id: "hero" }).navigation.zoom({
    target: "fullscreen",
  });
};
This generates:
  • Transform that expands the element to fill the screen
  • Aspect ratio preservation
  • Smooth interpolation of scale and translation
Perfect for hero/detail transitions where an image or card expands to dominate the new screen.

Advanced Bounds Options

When calling bounds(), pass an options object:
bounds({
  id: "avatar",           // Element ID
  method: "transform",    // "transform", "size", or "content"
  space: "relative",      // "relative" or "absolute"
  scaleMode: "match",     // "match", "none", or "uniform"
  group: "detail",        // Group for organized flows (NEW)
  target: "fullscreen",   // For navigation zoom
  anchor: "center",       // Anchor point
  raw: false,             // Get raw bounds without transforms
})

Method Options

MethodPurpose
"transform"Use GPU transforms for animation (default, best performance)
"size"Animate layout size changes
"content"Animate content reflow

Scale Mode

ModeBehavior
"match"Match source and destination scales exactly (default)
"none"No scaling, only translation
"uniform"Scale uniformly on both axes

Groups (NEW in 3.4)

Organize shared elements into groups for complex flows:
// Paged layout
bounds({ id: "card-1", group: "paged" })
bounds({ id: "card-2", group: "paged" })

// Master-detail flow
bounds({ id: "item", group: "detail" })
Groups help the library optimize animations when multiple shared elements exist.

Getting Bounds Without Animation

Access bounds without triggering animation:
const snapshot = bounds.getSnapshot("avatar");
if (snapshot) {
  console.log(snapshot.x, snapshot.y, snapshot.width, snapshot.height);
}
Useful in deferred animations where you need bounds data before animation starts.

Deferred Animation with Bounds

Return "defer" to wait for bounds to be measured:
const deferredInterpolator = ({ progress, bounds }) => {
  "worklet";

  // Check if source bounds are available
  const snapshot = bounds.getSnapshot("hero");
  if (!snapshot) {
    return "defer";  // Wait until bounds are measured
  }

  // Now safe to animate
  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
  };
};

Creating Boundary Components (NEW in 3.4)

Wrap custom components with boundary functionality:
import { Transition } from 'react-native-screen-transitions';

const CustomImage = ({ source, ...props }) => (
  <Image source={source} {...props} />
);

export const SharedImage = Transition.createBoundaryComponent(CustomImage, {
  alreadyAnimated: false,
});

// Usage
<SharedImage id="avatar" source={{ uri: '...' }} />
Options:
  • alreadyAnimated?: boolean - Set true if component already applies animations

Deprecated sharedBoundTag

The sharedBoundTag prop is deprecated. Migrate to boundary components:
// ❌ Deprecated
<Transition.View sharedBoundTag="avatar">
  <Image {...} />
</Transition.View>

// ✅ New approach
<Transition.Boundary.View id="avatar">
  <Image {...} />
</Transition.Boundary.View>
The old syntax still works but should not be used in new code.

Masked View Setup (Deprecated)

Transition.MaskedView is deprecated. Use navigationMaskEnabled instead:
// ❌ Deprecated
<Transition.MaskedView>
  <Transition.Stack>
    {/* screens */}
  </Transition.Stack>
</Transition.MaskedView>

// ✅ New approach
<Transition.Stack navigationMaskEnabled>
  {/* screens */}
</Transition.Stack>
See Masked View Setup for legacy usage if needed. Automatically applies masking for shared element reveal effects:
<Transition.Stack
  navigationMaskEnabled
  screenStyleInterpolator={screenStyleInterpolator}
  gestureEnabled
>
  {/* screens */}
</Transition.Stack>
This provides built-in visual masking without manual setup, perfect for shared element reveals.

Shared Elements with Gestures

Combine shared elements with interactive gestures:
const interactiveSharedInterpolator = ({ progress, bounds }) => {
  "worklet";

  const b = bounds({ id: "hero" });

  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
    "hero": {
      style: {
        borderRadius: interpolate(progress, [0, 1], [12, 0]),
      },
    },
  };
};

<Transition.Stack
  screenStyleInterpolator={interactiveSharedInterpolator}
  gestureEnabled
  gestureDirection="vertical"
>
  {/* screens */}
</Transition.Stack>
Users can now drag to interactively drive the shared element animation.

Multiple Shared Elements

Animate multiple elements simultaneously:
const multiSharedInterpolator = ({ progress, bounds }) => {
  "worklet";

  return {
    content: {
      style: {
        opacity: interpolate(progress, [0, 1], [0, 1]),
      },
    },
    "avatar": {
      style: {
        borderRadius: interpolate(progress, [0, 1], [40, 0]),
      },
    },
    "title": {
      style: {
        fontSize: interpolate(progress, [0, 1], [16, 24]),
      },
    },
    "description": {
      style: {
        opacity: interpolate(progress, [0, 1], [0.5, 1]),
      },
    },
  };
};
Source and destination boundaries:
// Source screen
<>
  <Transition.Boundary.Pressable id="avatar" onPress={navigate}>
    <Image {...} />
  </Transition.Boundary.Pressable>
  <Transition.Boundary.Pressable id="title" onPress={navigate}>
    <Text>Title</Text>
  </Transition.Boundary.Pressable>
</>

// Destination screen
<>
  <Transition.Boundary.View id="avatar">
    <Image {...} />
  </Transition.Boundary.View>
  <Transition.Boundary.View id="title">
    <Text>Title</Text>
  </Transition.Boundary.View>
</>

Troubleshooting Shared Elements

Bounds Not Measuring

Ensure boundary components are rendered:
// ✅ Visible and measurable
<Transition.Boundary.View id="avatar">
  <Image style={{ width: 100, height: 100 }} />
</Transition.Boundary.View>

// ❌ Not visible
{false && <Transition.Boundary.View id="avatar">...</Transition.Boundary.View>}

Animation Jumps

Use "defer" to wait for measurement:
const deferredInterpolator = ({ bounds }) => {
  "worklet";
  if (!bounds.getSnapshot("avatar")) return "defer";
  // Animation continues once bounds are ready
};

Bounds Mismatch

Ensure source and destination have matching IDs:
// Source
<Transition.Boundary.Pressable id="hero">...</Transition.Boundary.Pressable>

// Destination
<Transition.Boundary.View id="hero">...</Transition.Boundary.View>

// Interpolator
bounds({ id: "hero" })

Next Steps