Core Concepts

Bounds Transitions

Use Transition.Boundary and the scoped bounds accessor to sync geometry across screens.

What This System Is

Bounds Transitions animate matched geometry across screens by measuring a source boundary and a destination boundary, then moving screen A and screen B together so the result reads like shared element motion.

If you are specifically looking for true shared element transitions, wait for Reanimated Shared Element Transitions to become production-ready. This package's bounds system is a different approach.

Boundary Pattern

Use Transition.Boundary for both interactive and passive boundaries.

When onPress is present, the boundary behaves like a pressable source. When onPress is omitted, it behaves like a passive measured view.

TSX

1// Source screen
2<Transition.Boundary
3 id="hero"
4 onPress={() => navigation.navigate("Detail")}
5>
6 <Image source={heroImage} style={{ width: 120, height: 160 }} />
7</Transition.Boundary>
8
9// Destination screen
10<Transition.Boundary id="hero">
11 <Image source={heroImage} style={{ width: "100%", height: 360 }} />
12</Transition.Boundary>

For the high-level bounds(id).navigation.zoom() helper, the destination boundary is optional unless the zoom should target a specific destination element. See Zoom.

Nested Measurement with Boundary.Target

Use Transition.Boundary.Target when the pressable owner is larger than the visual element you want to measure.

TSX

1<Transition.Boundary
2 id={item.id}
3 anchor="leading"
4 scaleMode="uniform"
5 style={styles.row}
6 onPress={() => navigation.navigate("Playlist")}
7>
8 <Transition.Boundary.Target style={styles.artTarget}>
9 <Image source={item.image} style={styles.artwork} />
10 </Transition.Boundary.Target>
11
12 <View style={styles.copy}>
13 <Text>{item.title}</Text>
14 </View>
15</Transition.Boundary>

This keeps the row as the interaction owner while limiting measurement to the nested artwork.

The bounds() Accessor

Inside screenStyleInterpolator, call bounds(id) to scope the accessor to one boundary identity.

TSX

1screenStyleInterpolator: ({ bounds }) => {
2 "worklet";
3
4 return {
5 hero: {
6 style: bounds("hero").styles(),
7 },
8 };
9};

The scoped accessor exposes:

  • bounds(id).styles(options) for render-ready animated styles
  • bounds(id).values(options) for numeric geometry
  • bounds(id).link(id?) for inspecting the active source/destination link
  • bounds(id).navigation.zoom(options) for the built-in zoom recipe
  • bounds(id).navigation.reveal(options) for the built-in reveal recipe

Bound identities can be a string, a number, or an object with { id, group }.

TSX

1const card = bounds({ group: "gallery", id: item.id });
2const style = card.styles({ method: "size" });
3const link = card.link();

Bounds Options

Pass identity to bounds(), then pass compute options to styles() or values().

OptionValuesNotes
method"transform" | "size" | "content"Matching strategy
target"bound" | "fullscreen" | MeasuredDimensionsDestination target
anchor"topLeading" through "bottomTrailing"Matching anchor point
scaleMode"match" | "none" | "uniform"Aspect-ratio behavior
offset{ x?: number; y?: number }Add live x/y offsets from gestures, scroll, or animation state
progressnumberCustom progress driver for the bounds interpolation
motionBoundsMotionReplace the generated transform path without reimplementing measurement
space"relative" | "absolute"absolute is mainly for navigation helpers and custom renderers

Numeric Bounds with values()

Use styles() when you want the library to return the final animated style object.

Use values() when you want the same measured geometry but need to compose the final style yourself.

TSX

1screenStyleInterpolator: ({ bounds, current }) => {
2"worklet";
3
4const hero = bounds("hero").values({
5 method: "size",
6 space: "absolute",
7 progress: current.transitionProgress,
8});
9
10return {
11 hero: {
12 style: {
13 width: hero.width,
14 height: hero.height,
15 transform: [
16 { translateX: hero.translateX },
17 { translateY: hero.translateY },
18 ],
19 },
20 },
21};
22};

Bounds use gesture-driven progress by default. When you add freeform gesture movement separately, that progress can pull the geometry back toward the transition path and create a magnetic feel. Pass transitionProgress to keep the bounds geometry on its committed path while your gesture values control the extra movement.

Zoom and Reveal

For navigation-style bounds transitions, use the navigation helpers:

TSX

1screenStyleInterpolator: ({ bounds }) => {
2 "worklet";
3
4 return bounds("hero").navigation.zoom();
5};

For masked source-to-container transitions, use navigation.reveal():

TSX

1screenStyleInterpolator: ({ bounds, active }) => {
2 "worklet";
3
4 const id = active.route.params?.revealId;
5 if (!id) return null;
6
7 return bounds(id).navigation.reveal({
8 borderRadius: 48,
9 });
10};

See Zoom and Reveal for the full setup.

Groups

group is for flows where the active member can change inside the same collection, such as galleries, paged detail screens, or retargeted list/detail flows.

TSX

1// Source
2<Transition.Boundary group="gallery" id={item.id} onPress={openItem}>
3 <Image source={item.image} />
4</Transition.Boundary>
5
6// Destination
7<Transition.Boundary group="gallery" id={item.id}>
8 <Image source={item.image} />
9</Transition.Boundary>

The group keeps track of the active member id so the transition stays aligned as focus moves between items in the same collection.

Live Payload Handoff

Use handoff when the same live payload should move between matching boundaries instead of remounting on each screen.

TSX

1// Source
2<Transition.Boundary id="video" handoff onPress={openPlayer}>
3 <Transition.Boundary.Target style={styles.video}>
4 <VideoView player={player} style={StyleSheet.absoluteFill} />
5 </Transition.Boundary.Target>
6</Transition.Boundary>
7
8// Destination
9<Transition.Boundary id="video" handoff>
10 <Transition.Boundary.Target style={styles.heroVideo} />
11</Transition.Boundary>

Every matching boundary that should participate in the handoff should set handoff. The destination defines the receiving bounds; it does not need to render a second copy of the live child.

Use escapeClipping when a boundary needs to render outside local clipping or layout constraints during the transition.

TSX

1<Transition.Boundary
2 id="card"
3 escapeClipping
4 onPress={() => navigation.navigate("Detail")}
5>
6 <Image source={image} style={styles.cardImage} />
7</Transition.Boundary>

handoff and escapeClipping use react-native-teleport under the hood. Install the supported version when using either prop:

npm install react-native-teleport@1.2.0

Version 1.2.0 or later provides the native reparenting path used by live boundary handoff.

Bounds with Offsets

Because styles() and values() accept an offset option, you can feed any live x/y values into the matched transform.

TSX

1screenStyleInterpolator: ({ bounds, active }) => {
2 "worklet";
3
4 return {
5 hero: {
6 style: bounds("hero").styles({
7 offset: {
8 x: active.gesture.x,
9 y: active.gesture.y,
10 },
11 target: "fullscreen",
12 }),
13 },
14 };
15};

Bounds with a Custom Progress Driver

By default, bounds use the visual frame progress, which includes live gesture movement.

Pass transitionProgress when the geometry should stay on the committed transition path while you layer gesture motion separately:

TSX

1screenStyleInterpolator: ({ bounds, current }) => {
2 "worklet";
3
4 return {
5 hero: {
6 style: bounds("hero").styles({
7 progress: current.transitionProgress,
8 offset: {
9 x: current.gesture.x,
10 y: current.gesture.y,
11 },
12 }),
13 },
14 };
15};

This is the replacement for older gestureProgressMode: "freeform" patterns in custom bounds animations.

Use link() when you need to inspect the active source/destination pair.

TSX

1const hero = bounds("hero");
2const link = hero.link();
3const initialSource = link?.initialSource;
4const initialDestination = link?.initialDestination;

Most animations should use styles(), values(), or the navigation helpers instead of reading the link directly. link() is useful when you are building custom choreography on top of the same measured pair.

Troubleshooting

Bounds Mismatch

Source, destination, and interpolator lookups must all resolve to the same boundary identity.

TSX

1// Source
2<Transition.Boundary id="hero">...</Transition.Boundary>
3
4// Destination
5<Transition.Boundary id="hero">...</Transition.Boundary>
6
7// Interpolator
8bounds("hero")

If you use groups, the same rule applies to the group + id pair.