---
modificationDate: April 02, 2026
title: Zoom transition
description: Learn how to use the zoom transition to create fluid animations between screens when using Expo Router for iOS.
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/router/advanced/zoom-transition/","feedback":"🤖 Agent feedback: <specific, actionable description>"}'

Only submit when you have something specific and actionable to report.

</AgentInstructions>

# Zoom transition

Learn how to use the zoom transition to create fluid animations between screens when using Expo Router for iOS.

> For the complete documentation index, see [llms.txt](/llms.txt). Use this file to discover all available pages.

> Zoom transition is an [alpha](/more/release-statuses#alpha) API available on **iOS only** in **Expo SDK 55** and later. The API is subject to breaking changes.

Zoom transitions provide a fluid animation effect when navigating between screens by zooming from a source element to the destination screen. This feature leverages iOS 18+ native zoom transition API to create shared, interactive transitions that produce a sense of spatial awareness between routes. For example, a card thumbnail may transition to become a full-width banner on the next route.

## Get started

To implement zoom transitions, you need to use the `Link.AppleZoom` component to mark the source element and optionally `Link.AppleZoomTarget` to specify the target alignment on the destination screen.

### Basic example

To activate zoom transition for a link, wrap the source (`Image`) element with `Link.AppleZoom` in your screen:

```tsx
import { View, Text, StyleSheet, Pressable } from 'react-native';
import { Link } from 'expo-router';
import { Image } from 'expo-image';

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <Link href="/image" asChild>
        <Link.AppleZoom>
          <Pressable>
            <Image
              source={{ uri: 'https://example.com/image-1.jpg' }}
              style={{ width: 100, height: 200 }}
            />
          </Pressable>
        </Link.AppleZoom>
      </Link>
    </View>
  );
}
```

In the destination screen, define the `Image` component:

```tsx
import { View, Text, StyleSheet } from 'react-native';
import { Image } from 'expo-image';

export default function DetailsScreen() {
  return <Image source={{ uri: 'https://example.com/image-1.jpg' }} style={{ flex: 1 }} />;
}
```

## Using `Link.AppleZoom`

The `Link.AppleZoom` component wraps the element you want to zoom from. It is useful for marking the source of the zoom transition, if you want to include additional elements alongside the zoomed content.

```tsx
<Link href="/image" asChild>
  <Pressable>
    <Link.AppleZoom>
      <View>{/* Your content */}</View>
    </Link.AppleZoom>
    <Text>Subtitle</Text>
  </Pressable>
</Link>
```

> `Link.AppleZoom` only accepts a single child component. If you need to wrap multiple children, use a `View` or another container component.

### Customizing alignment

You can specify the alignment of the zoomed element on the destination screen by using `Link.AppleZoomTarget` element.

```tsx
export default function ImageScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Link.AppleZoomTarget>
        <Image source={{ uri: 'https://example.com/image-1.jpg' }} style={{ width: '100%' }} />
      </Link.AppleZoomTarget>
    </View>
  );
}
```

If you need more control over the alignment rectangle, you can pass an `alignmentRect` prop to `Link.AppleZoom`. However, this is normally not necessary if you use `Link.AppleZoomTarget`.

> The `alignmentRect` prop internally relies on [`alignmentRectProvider`](https://developer.apple.com/documentation/uikit/uiviewcontroller/transition/zoomoptions/alignmentrectprovider) API.

```tsx
<Link.AppleZoom alignmentRect={{ x: 0, y: 0, width: 200, height: 300 }}>
  <Image source={{ uri: 'https://example.com/image-1.jpg' }} style={{ width: 100, height: 150 }} />
</Link.AppleZoom>
```

## Complete example

Here's a more complex example showing a gallery grid with zoom transitions to detail views. The source screen component (**src/app/index.tsx**) uses `Link.AppleZoom` to wrap an `Image` component:

```tsx
import { Image } from 'expo-image';
import { Link } from 'expo-router';
import { useState } from 'react';
import { Text, Pressable, ScrollView, StyleSheet } from 'react-native';

const IMAGES = [
  // Define your array of images here.
];

export default function Index() {
  return (
    <ScrollView
      style={styles.scrollView}
      contentContainerStyle={styles.scrollViewContent}
      contentInsetAdjustmentBehavior="automatic">
      {IMAGES.map((_, index) => (
        <Thumbnail key={index} index={index} />
      ))}
    </ScrollView>
  );
}

function Thumbnail({ index }: { index: number }) {
  const [size, setSize] = useState<{ width: number; height: number } | null>(null);
  return (
    <Link
      href={{
        pathname: `/image/[id]`,
        // You need to pass the image size to the detail page, so that the layout can be measured during the first render.
        params: { id: index, width: size?.width, height: size?.height },
      }}
      asChild>
      <Pressable style={styles.thumbnail}>
        <Link.AppleZoom>
          <Image
            source={IMAGES[index % IMAGES.length]}
            style={styles.thumbnailImage}
            onLoad={e => setSize({ width: e.source.width, height: e.source.height })}
          />
        </Link.AppleZoom>
        <Text style={{ textAlign: 'center' }}>Photo {index + 1}</Text>
      </Pressable>
    </Link>
  );
}

const styles = StyleSheet.create({
  scrollView: {
    flex: 1,
    backgroundColor: '#fff',
    padding: 16,
  },
  scrollViewContent: {
    justifyContent: 'center',
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 16,
  },
  thumbnail: {
    width: 170,
    aspectRatio: 1,
  },
  thumbnailImage: {
    width: '100%',
    height: '100%',
    borderRadius: 8,
  },
});
```

In the destination screen, the `Link.AppleZoomTarget` is used to specify the alignment of the zoomed element:

```tsx
import { Image } from 'expo-image';
import { Link, useLocalSearchParams } from 'expo-router';
import { useMemo } from 'react';
import { StyleSheet, useWindowDimensions, View } from 'react-native';

export default function ImagePage() {
  const params = useLocalSearchParams();
  const index = params.id ? parseInt(params.id as string, 10) : 0;
  const imageSource = IMAGES[index % IMAGES.length];
  const imageSize = {
    width: parseInt(params.width as string, 10),
    height: parseInt(params.height as string, 10),
  };
  const windowDimensions = useWindowDimensions();
  // Compute the size to fit within the window while maintaining aspect ratio.
  const computedSize = useMemo(() => {
    if (!imageSize.width || !imageSize.height) {
      return { width: windowDimensions.width, height: windowDimensions.height };
    }
    const widthRatio = windowDimensions.width / imageSize.width;
    const heightRatio = windowDimensions.height / imageSize.height;
    const minRatio = Math.min(widthRatio, heightRatio);
    return {
      width: imageSize.width * minRatio,
      height: imageSize.height * minRatio,
    };
  }, [imageSize, windowDimensions]);

  return (
    <View style={styles.container}>
      <Link.AppleZoomTarget>
        <View style={{ ...computedSize }}>
          <Image source={imageSource} style={styles.image} />
        </View>
      </Link.AppleZoomTarget>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    justifyContent: 'center',
    alignItems: 'center',
  },
  image: {
    width: '100%',
    height: '100%',
  },
});
```

## Controlling dismissal gestures

The [`usePreventZoomTransitionDismissal`](/versions/latest/sdk/router/link#usepreventzoomtransitiondismissal_options) hook allows you to control the interactive swipe-to-dismiss gesture on screens using zoom transitions. This is useful when you want to prevent accidental dismissals or restrict dismissal to specific screen areas.

### Disabling dismissal completely

Call the hook without any options to completely disable the swipe-to-dismiss gesture:

```tsx
import { usePreventZoomTransitionDismissal } from 'expo-router';

export default function DetailScreen() {
  usePreventZoomTransitionDismissal();
  // Dismissal gesture is now disabled - users must use navigation controls to go back
  return <View>{/* Content */}</View>;
}
```

### Restricting dismissal to a specific area

Use the `unstable_dismissalBoundsRect` option to define a rectangle where dismissal gestures are allowed. This is useful for image viewers where you want dismissal only from the image area:

```tsx
import { usePreventZoomTransitionDismissal } from 'expo-router';
import { View, StyleSheet } from 'react-native';
import { Image } from 'expo-image';

export default function DetailScreen() {
  // Only allow dismissal gestures that start within this rectangle
  usePreventZoomTransitionDismissal({
    unstable_dismissalBoundsRect: { minX: 100, minY: 100, maxX: 300, maxY: 300 },
  });

  return (
    <View style={styles.container}>
      {/* Visual indicator of the dismissal zone (for demonstration) */}
      <View style={styles.dismissalZone} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  image: {
    flex: 1,
  },
  dismissalZone: {
    position: 'absolute',
    left: 100,
    top: 100,
    width: 200, // maxX - minX = 300 - 100
    height: 200, // maxY - minY = 300 - 100
    borderWidth: 2,
    borderColor: 'rgba(0, 122, 255, 0.5)',
    borderStyle: 'dashed',
    backgroundColor: 'rgba(0, 122, 255, 0.1)',
  },
});
```

> The `unstable_dismissalBoundsRect` option internally relies on the [`interactiveDismissShouldBegin`](https://developer.apple.com/documentation/uikit/uiviewcontroller/transition/zoomoptions/interactivedismissshouldbegin) API.

## Platform support

Zoom transitions is only available on iOS 18 and later. On older iOS versions or other platforms, the component will render normally without the zoom animation effect.

The zoom transition components automatically detect platform support and gracefully degrade to standard navigation on unsupported platforms.

## Known limitations

Using zoom transition with headers

We recommend avoiding the use of zoom transitions when navigating between screens that have a header (navigation bar). There are known issues with the native iOS zoom transition API that can lead to visual glitches or unexpected behavior when headers are involved.

Using zoom transition with Link.Preview

When using `Link.Preview` in combination with zoom transitions, the target screen must use the modal presentation, for example, `presentation: 'fullScreenModal'`. This is the limitation of the underlying iOS zoom transition API. When navigating to a non-modal screen from a `Link.Preview`, the zoom transition will not work as expected and will fall back to a standard navigation transition.

`usePreventZoomTransitionDismissal` cannot be used in screens with modal presentation

The `usePreventZoomTransitionDismissal` hook cannot be used in screens that have a modal presentation, for example, `presentation: 'fullScreenModal'`. When used in a modal screen, the hook will not have any effect and dismissal gestures will function as normal.

Single child requirement

Both `Link.AppleZoom` and `Link.AppleZoomTarget` only accept a single child component. If you attempt to pass multiple children, a warning will be logged and the component will not render properly.

**Incorrect:**

```tsx
<Link.AppleZoom>
  <View />
  <Text />
</Link.AppleZoom>
```

**Correct:**

```tsx
<Link.AppleZoom>
  <View>
    <Image />
    <Text />
  </View>
</Link.AppleZoom>
```

Noticeable delay when opening or dismissing screens

You may experience a noticeable delay (approximately 1 second) when navigating to or dismissing screens that use zoom transitions, especially when performing rapid open/close/open gestures. This latency is higher than what you would see with native iOS apps using the same zoom transition API.

This is an upstream issue in `react-native-screens` related to how it handles transitions on iOS. The Expo team is actively working with the `react-native-screens` team to improve this. See this [GitHub Issue](https://github.com/expo/expo/issues/42797) for updates and more details.

Supported only within router's Stack navigator

The zoom transition feature is only supported when using the router's built-in Stack navigator. If you attempt to use Link with zoom transition to a screen that is not part of a Stack navigator, the zoom transition will not work as expected.

Must be used within a Link

`Link.AppleZoom` must be used as a direct or indirect child of a `Link` component with the `asChild` prop. Using it outside this context will result in an error.

iOS 18+ only

The zoom transition feature requires iOS 18 or later. While the components will render on older versions, the zoom animation will not be applied.
