One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Zoom Transitions

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.

Usage

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.

How it works

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.

ZoomTransitionEnabler

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>
</>
)
}
PropsTypeDescription
zoomTransitionSourceIdentifier (required)stringThe source that the screen grows out of.
stringThe source that the screen grows out of.
dismissalBoundsRectDismissalBoundsRect | nullScreen-space bounds, in points, that constrain where the dismiss gesture can release.
DismissalBoundsRect | nullScreen-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.

DismissalBoundsRectTypeDescription
minXnumberOptional minimum horizontal gesture location.
numberOptional minimum horizontal gesture location.
maxXnumberOptional maximum horizontal gesture location.
numberOptional maximum horizontal gesture location.
minYnumberOptional minimum vertical gesture location.
numberOptional minimum vertical gesture location.
maxYnumberOptional maximum vertical gesture location.
numberOptional maximum vertical gesture location.

ZoomTransitionSource

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>
)
}
PropsTypeDescription
identifier (required)stringNames this source for the enabler.
stringNames this source for the enabler.
alignmentSourceAlignmentRectA fixed rect, { x, y, width, height } in points, to zoom out of instead of the child.
SourceAlignmentRectA 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.
booleanAnimates a ratio change between source and landing rects.
SourceAlignmentRectTypeDescription
xnumberHorizontal source origin in points.
numberHorizontal source origin in points.
ynumberVertical source origin in points.
numberVertical source origin in points.
widthnumberSource rectangle width in points.
numberSource rectangle width in points.
heightnumberSource rectangle height in points.
numberSource rectangle height in points.

ZoomTransitionAlignmentRectDetector

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>
)
}
PropsTypeDescription
identifier (required)stringThe source identifier this landing rect belongs to.
stringThe source identifier this landing rect belongs to.

Platform differences

iOSAndroidWeb
Push, 18 upZoomsNullNull
Older iOSPlain pushNoneNone
Dismiss, 18 upShrinks backNullNull

Limits and gotchas

Credit

The native zoom transition is adapted from Expo Router.

Edit this page on GitHub.