Native iOS 18 zoom transitions between a source view and a pushed screen
One.iOS.ZoomTransitionEnabler, One.iOS.ZoomTransitionSource, and
One.iOS.ZoomTransitionAlignmentRectDetector build a native iOS 18 zoom
transition: a pushed screen grows out of a source view, and the dismissal
shrinks back into it. The enabler mounts in the pushed screen’s content and
names the source; the source wraps the originating view; the optional
detector measures the landing view.
Reach for it for media and card flows where the detail should bloom from its thumbnail. The transition needs iOS 18 or newer; older versions fall back to the standard push animation, and off iOS every part renders null.
import { Image } from 'react-native'import { One } from 'one'
function Thumbnail() { return ( <One.iOS.ZoomTransitionSource identifier="photo-1"> <Image source={{ uri: 'https://example.com/photo.jpg' }} style={{ width: 120, height: 120 }} /> </One.iOS.ZoomTransitionSource> )}
function PhotoScreen() { return ( <> <One.iOS.ZoomTransitionEnabler zoomTransitionSourceIdentifier="photo-1" /> <One.iOS.ZoomTransitionAlignmentRectDetector identifier="photo-1"> <Image source={{ uri: 'https://example.com/photo.jpg' }} style={{ width: '100%', height: 400 }} /> </One.iOS.ZoomTransitionAlignmentRectDetector> </> )}The screen zooms out of the thumbnail on push and back into it on dismissal, landing on the detector’s measured rect.
The enabler finds its view controller and sets UIKit’s zoom preferred transition naming the source identifier, on iOS 18 or newer. The source registers its single native child under the identifier; the detector registers its child as the landing view. On push the incoming screen grows out of the source’s rect toward the landing rect, and on dismissal it reverses. Registration is by identifier in a shared native repository, and unmounting a source unregisters it, so the source and the pushed screen must both stay mounted for the round trip.
Mount one inside the pushed screen’s content, anywhere in its tree. It is invisible: it sets the transition and renders a 1 point box. Without an identifier it logs and does nothing.
import { Text } from 'react-native'import { One } from 'one'
function Detail({ title }: { title: string }) { return ( <> <One.iOS.ZoomTransitionEnabler zoomTransitionSourceIdentifier="photo-1" /> <Text>{title}</Text> </> )}| Props | Type | Description |
|---|---|---|
zoomTransitionSourceIdentifier (required)stringThe source that the screen grows out of. | string | The source that the screen grows out of. |
dismissalBoundsRectDismissalBoundsRect | nullScreen-space bounds, in points, that constrain where the dismiss gesture can release. | DismissalBoundsRect | null | Screen-space bounds, in points, that constrain where the dismiss gesture can release. |
Before the push or sheet that should zoom, set
screenOptions={{ animation: 'none' }} or use a sheet without its own
animation. The zoom transition is the animation; leaving the default in
place plays both at once.
| DismissalBoundsRect | Type | Description |
|---|---|---|
minXnumberOptional minimum horizontal gesture location. | number | Optional minimum horizontal gesture location. |
maxXnumberOptional maximum horizontal gesture location. | number | Optional maximum horizontal gesture location. |
minYnumberOptional minimum vertical gesture location. | number | Optional minimum vertical gesture location. |
maxYnumberOptional maximum vertical gesture location. | number | Optional maximum vertical gesture location. |
Wrap the originating view so the pushed screen grows out of it on presentation and shrinks back into it on dismissal. The source takes a single native child; extra children log and are ignored, and unmounting the child unregisters the source.
import { Image } from 'react-native'import { One } from 'one'
function ThumbnailWithRatio() { return ( <One.iOS.ZoomTransitionSource identifier="photo-1" animateAspectRatioChange> <Image source={{ uri: 'https://example.com/photo.jpg' }} style={{ width: 120, height: 120 }} /> </One.iOS.ZoomTransitionSource> )}| Props | Type | Description |
|---|---|---|
identifier (required)stringNames this source for the enabler. | string | Names this source for the enabler. |
alignmentSourceAlignmentRectA fixed rect, { x, y, width, height } in points, to zoom out of instead of the child. | SourceAlignmentRect | A fixed rect, { x, y, width, height } in points, to zoom out of instead of the child. |
animateAspectRatioChangebooleanAnimates a ratio change between source and landing rects. | boolean | Animates a ratio change between source and landing rects. |
| SourceAlignmentRect | Type | Description |
|---|---|---|
xnumberHorizontal source origin in points. | number | Horizontal source origin in points. |
ynumberVertical source origin in points. | number | Vertical source origin in points. |
widthnumberSource rectangle width in points. | number | Source rectangle width in points. |
heightnumberSource rectangle height in points. | number | Source rectangle height in points. |
Optionally wrap the landing view so the transition aims at its measured rect instead of the screen. It takes a single native child; changing its identifier later logs and is ignored.
import { Image } from 'react-native'import { One } from 'one'
function Landing() { return ( <One.iOS.ZoomTransitionAlignmentRectDetector identifier="photo-1"> <Image source={{ uri: 'https://example.com/photo.jpg' }} style={{ width: '100%', height: 400 }} /> </One.iOS.ZoomTransitionAlignmentRectDetector> )}| Props | Type | Description |
|---|---|---|
identifier (required)stringThe source identifier this landing rect belongs to. | string | The source identifier this landing rect belongs to. |
| iOS | Android | Web | |
|---|---|---|---|
| Push, 18 up | Zooms | Null | Null |
| Older iOS | Plain push | None | None |
| Dismiss, 18 up | Shrinks back | Null | Null |
animation: 'none' on the pushed
screen or the default transition plays alongside the zoom.The native zoom transition is adapted from Expo Router.
Edit this page on GitHub.