v3.9 focuses on making shared bounds more predictable.
The bounds system now stays closer to the latest measured source and destination. This improves retargeting when a destination moves, resizes, or changes scale, and it gives boundary handoff a simpler public model.
The main API changes are:
Transition.Boundaryis now the primary boundary component.bounds(id).styles()returns render-ready animated styles.bounds(id).values()returns numeric values for custom renderers and interpolation logic.handoffmoves a live boundary payload between matching boundaries.escapeClippinglets a boundary render outside local clipping constraints while it transitions.navigation.zoom()now owns its native-style opacity, drag, mask, pinch, and release behavior while exposing a smaller set of intensity controls.Transition.Specs.Zoomprovides the tuned open and close springs for navigation zoom transitions.- Pinch gestures now expose their fixed activation point as
pinchOriginXandpinchOriginY. contentComponentadds a custom renderer for the screen content layer.backdropComponentcan now receive render props:{ styles, props, pointerEvents }.surfaceComponentis deprecated in favor ofcontentComponent.blockTransition()andunblockTransition()can hold a pending transition until the destination is ready to animate.
Layer Components
v3.9 adds contentComponent for custom screen shells.
Use it when the screen content layer needs to be rendered by your own component, while still receiving the library-owned animated styles, animated props, pointer events, and children.
TSX
backdropComponent keeps accepting a component type, but function components can now render the layer manually:
TSX
This replaces the older backdrop shape that split animated props and visual styles across multiple view layers.
surfaceComponent still works for compatibility, but it is deprecated. New code should move custom screen shells to contentComponent.
Delaying transition start
blockTransition() and unblockTransition() are named exports for coordinating a transition with destination-screen work.
TSX
Calls are reference-counted. Every blockTransition() call must be paired with one unblockTransition() call for the same route, so release the block in finally when the work can fail. When the route key is omitted, both functions use the most recently focused screen in navigation history.
This is visual coordination, not a performance optimization. It does not reduce rendering work or move that work off the JavaScript thread. It keeps a pending transition at its initial frame until the app is ready to show it.
Boundary API
Transition.Boundary is now the component to reach for first.
When onPress is present, it behaves like a pressable boundary. When onPress is omitted, it behaves like a passive view boundary.
The old split made sense in a simple A to B example: the source was a Trigger, and the destination was a View.
That model got harder to explain once bounds started working across more than two screens. A boundary can be the thing you press, a placeholder that keeps layout stable, the destination that receives a handoff, or a matched element several screens away. Calling some of those View and some of those Trigger made the API feel more specific than the system actually is.
The new component is intentionally plain: it is a boundary with an id. Add onPress when that boundary should handle a press. Leave onPress off when it should only measure and participate in the transition.
TSX
Transition.Boundary.View and Transition.Boundary.Trigger still work, but they are deprecated aliases. New code should use Transition.Boundary.
TSX
Handoff
Use handoff when the same live payload should move between matching boundaries during a transition.
handoff remains experimental in v3.9. Native-backed content can expose platform compositor and lifecycle edge cases, especially Android video surfaces. escapeClipping does not move a live payload and is not covered by that warning.
Every matching boundary that should participate in that handoff should set handoff. Treat it as a contract between the source and destination, not as a source-only flag. In a multi-screen chain, put it on each screen's matching boundary so the payload can keep moving from A to B, then B to C, and back again.
TSX
The destination uses the same boundary id and also opts into handoff, but it does not render another copy of the live child. Its target defines the receiving bounds:
TSX
This is useful for native views such as video players, maps, or other content where remounting would be visible. The boundary keeps its measured layout slot in place while the single live payload can move to the matching boundary.
Escaping Clipping
Use escapeClipping when a boundary needs to render outside local clipping or layout constraints during the transition.
TSX
Use handoff and escapeClipping together when both behaviors are needed:
TSX
handoff and escapeClipping use react-native-teleport under the hood. If you use either prop, install the optional peer dependency:
The supported range is >=1.1.0 <1.1.11. Version 1.1.11 is excluded because its host-size synchronization can trail continuously animated boundary hosts. Pin 1.1.10 when using either feature.
Better Retargeting
Shared bounds now stay closer to the element you are actually animating to.
If a destination changes while it is on screen, the closing transition can animate back from that updated position instead of using stale layout from when the screen first opened.
This makes common flows feel more accurate:
- a detail screen resizes media before dismissing
- a selected item changes while another screen is open
- a matched element lives inside a nested screen
- a live payload moves through several matching boundaries
v3.9 also handles transformed boundaries more predictably. When an element is scaled or transformed in its own layout, generated bounds styles can preserve that visual shape while still moving between screens.
Navigation Zoom
navigation.zoom() now exposes appearance and drag-response controls without exposing the preset's internal curves:
TSX
drag.translation and drag.scale accept horizontal and vertical response multipliers. 0 disables a response on that axis, 1 preserves the preset, and values above 1 exaggerate it.
Use the new paired spec alongside the zoom helper so both opening and closing use the tuned timing:
TSX
When no target is provided, zoom expands into a full-width rectangle that preserves the source aspect ratio. target: "bound" stays opt-in and waits for the matching destination measurement instead of temporarily falling back to fullscreen.
Zoom now also tracks the initial pinch focal point while scale and rotation change. Custom interpolators can read the same fixed screen-coordinate pivot from gesture.pinchOriginX and gesture.pinchOriginY, including through gesture.handoff.
The following previous zoom options remain accepted by TypeScript for compatibility, but they are ignored:
debugfocusedElementOpacityunfocusedElementOpacitymaxSensitivityvelocityDepthgestureProgressModehorizontalDragScaleverticalDragScalehorizontalDragTranslationverticalDragTranslation
Behavior Changes
Screens now wait for the next screen to resolve before applying that screen's defined styles.
In practice, if screen A opens screen B, A will not receive B's defined styles until B has resolved. This avoids a class of early-frame jumps where the current screen could briefly react to styles from a destination that was not ready yet.
If you added guards, delays, manual refreshes, or other workarounds to avoid unwanted first-frame behavior, those workarounds should be less necessary in v3.9.
Scoped Bounds API
bounds() now scopes to one identity first. From there, choose the output you need.
Breaking change from v3.8
The scoped accessor is a source-breaking change for code that uses the v3.8 bounds helpers.
The old accessor mixed three jobs: reading arbitrary entries from the bounds registry, interpolating the active source and destination, and producing render styles. That made it possible to read measurements from one screen key while rendering a different transition pair. v3.9 resolves one boundary identity against the active navigation pair instead, so the store-shaped helpers were removed rather than kept as aliases with different semantics.
| v3.8 API | v3.9 replacement |
|---|---|
bounds({ id, group, ...options }) | bounds({ id, group }).styles(options) |
bounds.getLink(id) or bounds({ id, group }).getLink() | bounds({ id, group }).link() |
BoundsLink.compute(options) | Scope the identity again, then call bounds({ id, group }).styles(options) for render styles or .values(options) for generated numeric geometry |
interpolateBounds(...) | For generated geometry, use .values(options). To preserve scalar-property behavior, read link().source and link().destination, then interpolate their bounds values manually. |
interpolateStyle(...) | For generated render styles, use .styles(options). To preserve scalar-property behavior, read the active link and interpolate its numeric styles values manually. |
getMeasured(...) or getSnapshot(...) | Use bounds({ id, group }).link() and inspect the active link's source or destination entry |
For an ungrouped boundary, the shorter bounds(id) form is equivalent. Keep { id, group } together when migrating grouped boundaries. styles() and values() generate transition geometry; they are not direct scalar replacements for every property accepted by the old interpolation helpers.
The old screen-key overloads—getMeasured(..., key), getSnapshot(..., key), and interpolateBounds(..., targetKey)—have no direct replacement. Use matching boundary id and group values so the accessor resolves against the active screen pair.
TSX
The scoped accessor exposes:
bounds(id).styles(options)for animated style outputbounds(id).values(options)for numeric outputbounds(id).link(id?)for inspecting the active source/destination linkbounds(id).navigation.zoom(options)for the built-in zoom recipebounds(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
Numeric Bounds With values()
Use values() when you want the generated bounds values but need to compose the final style yourself.
TSX
Use styles() when you want the library to return the render-ready style object.
TSX
Bounds Motion
styles() and values() accept a motion resolver.
Use it when the measured start and end bounds are correct, but the path between them needs customization.
TSX
motion receives:
current: the generated bounds transform for this frameprogress: normalized bounds progress from0to1start: the resolved source rectend: the resolved destination rectprops: the active interpolation props
Return a replacement transform when an element needs an arc, depth, rotation, overshoot, or a path that should differ from the default source-to-destination line.
The returned transform supports:
xyscalerotaterotateXrotateYperspectivetransformOrigin
For method: "size", the returned scale is applied to the generated width and height. For values(), rotation fields are returned alongside the numeric bounds values.
Migration Notes
- Prefer
Transition.BoundaryoverTransition.Boundary.ViewandTransition.Boundary.Trigger. - Use
handoffon every matching boundary that should send or receive a live payload. - Use
escapeClippingwhen a boundary needs to render outside local clipping constraints during a transition. - Install
react-native-teleport@1.1.10if you usehandofforescapeClipping. - Replace
bounds({ id, ...options })style output withbounds(id).styles(options). - Replace raw numeric bounds usage with
bounds(id).values(options). - Replace earlier beta
bounds(id).math(options)usage withbounds(id).values(options).math()remains as a deprecated alias. - Replace bounds link helper calls with
bounds(id).link(). - Keep using
offset;gesturesstill exists as a deprecated alias for offset-style movement. - Use
motioninstead of reimplementing bounds measurement when an element needs custom arc, depth, rotation, or overshoot behavior. - Replace zoom's directional drag tuples with
drag.translationanddrag.scaleresponse multipliers. - Remove deprecated zoom opacity, sensitivity, velocity-depth, and debug options. They remain type-compatible in v3.9 but are ignored.
- Use
gesture.pinchOriginXandgesture.pinchOriginYwhen pinch-and-rotate motion needs a stable activation pivot.