- Created a new PostCSS configuration file to integrate Tailwind CSS. - Added a skills lock file containing various Expo skills with their respective source and computed hashes.
4.7 KiB
Apple Zoom Transitions
Fluid zoom transitions for navigating between screens. iOS 18+, Expo SDK 55+, Stack navigator only.
import { Link } from "expo-router";
Basic Zoom
Use withAppleZoom on Link.Trigger to zoom the entire trigger element into the destination screen:
<Link href="/photo" asChild>
<Link.Trigger withAppleZoom>
<Pressable>
<Image
source={{ uri: "https://example.com/thumb.jpg" }}
style={{ width: 120, height: 120, borderRadius: 12 }}
/>
</Pressable>
</Link.Trigger>
</Link>
Targeted Zoom with Link.AppleZoom
Wrap only the element that should animate. Siblings outside Link.AppleZoom are not part of the transition:
<Link href="/photo" asChild>
<Link.Trigger>
<Pressable style={{ alignItems: "center" }}>
<Link.AppleZoom>
<Image
source={{ uri: "https://example.com/thumb.jpg" }}
style={{ width: 200, aspectRatio: 4 / 3 }}
/>
</Link.AppleZoom>
<Text>Caption text (not zoomed)</Text>
</Pressable>
</Link.Trigger>
</Link>
Link.AppleZoom accepts only a single child element.
Destination Target
Use Link.AppleZoomTarget on the destination screen to align the zoom animation to a specific element:
// Destination screen (e.g., app/photo.tsx)
import { Link } from "expo-router";
export default function PhotoScreen() {
return (
<View style={{ flex: 1 }}>
<Link.AppleZoomTarget>
<Image
source={{ uri: "https://example.com/full.jpg" }}
style={{ width: "100%", aspectRatio: 4 / 3 }}
/>
</Link.AppleZoomTarget>
<Text>Photo details below</Text>
</View>
);
}
Without a target, the zoom animates to fill the entire destination screen.
Custom Alignment Rectangle
For manual control over where the zoom lands on the destination, use alignmentRect instead of Link.AppleZoomTarget:
<Link.AppleZoom alignmentRect={{ x: 0, y: 0, width: 200, height: 300 }}>
<Image source={{ uri: "https://example.com/thumb.jpg" }} />
</Link.AppleZoom>
Coordinates are in the destination screen's coordinate space. Prefer Link.AppleZoomTarget when possible — use alignmentRect only when the target element isn't available as a React component.
Controlling Dismissal
Zoom screens support interactive dismissal gestures by default (pinch, swipe down when scrolled to top, swipe from leading edge). Use usePreventZoomTransitionDismissal on the destination screen to control this.
Disable all dismissal gestures
import { usePreventZoomTransitionDismissal } from "expo-router";
export default function PhotoScreen() {
usePreventZoomTransitionDismissal();
return <Image source={{ uri: "https://example.com/full.jpg" }} />;
}
Restrict dismissal to a specific area
Use unstable_dismissalBoundsRect to prevent conflicts with scrollable content:
usePreventZoomTransitionDismissal({
unstable_dismissalBoundsRect: {
minX: 0,
minY: 0,
maxX: 300,
maxY: 300,
},
});
This is useful when the destination contains a zoomable scroll view — the system gives that scroll view precedence over the dismiss gesture.
Combining with Link.Preview
Zoom transitions work alongside long-press previews:
<Link href="/photo" asChild>
<Link.Trigger withAppleZoom>
<Pressable>
<Image
source={{ uri: "https://example.com/thumb.jpg" }}
style={{ width: 120, height: 120 }}
/>
</Pressable>
</Link.Trigger>
<Link.Preview />
</Link>
Best Practices
Good use cases:
- Thumbnail → full image (gallery, profile photos)
- Card → detail screen with similar visual content
- Source and destination with similar aspect ratios
Avoid:
- Skinny full-width list rows as zoom sources — the transition looks unnatural
- Mismatched aspect ratios between source and destination without
alignmentRect - Using zoom with sheets or popovers — only works in Stack navigator
- Hiding the navigation bar — known issues with header visibility during transitions
Tips:
- Always provide a close or back button — dismissal gestures are not discoverable
- If the destination has a zoomable scroll view, use
unstable_dismissalBoundsRectto avoid gesture conflicts - Source view doesn't need to match the tap target — only the
Link.AppleZoomwrapped element animates - When source is unavailable (e.g., scrolled off screen), the transition zooms from the center of the screen
References
- Expo Router Zoom Transitions: https://docs.expo.dev/router/advanced/zoom-transition/
- Link.AppleZoom API: https://docs.expo.dev/versions/v55.0.0/sdk/router/#linkapplezoom
- Apple UIKit Fluid Transitions: https://developer.apple.com/documentation/uikit/enhancing-your-app-with-fluid-transitions