Files
lamp/.agents/skills/building-native-ui/references/zoom-transitions.md
Seven 8963f777ee Add PostCSS configuration and skills lock file
- 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.
2026-03-09 06:41:01 +07:00

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_dismissalBoundsRect to avoid gesture conflicts
  • Source view doesn't need to match the tap target — only the Link.AppleZoom wrapped element animates
  • When source is unavailable (e.g., scrolled off screen), the transition zooms from the center of the screen

References