Skip to main content

Overview

Masked View is required for reveal-style transitions like SharedIGImage and SharedAppleMusic presets. The mask creates a “reveal” effect where content expands from the shared element bounds.
Masked View requires native code and will not work in Expo Go. You must use a development build or bare React Native.

Installation

1

Install the package

npx expo install @react-native-masked-view/masked-view
2

Rebuild your app

For Expo:
npx expo prebuild
npx expo run:ios
# or
npx expo run:android
For bare React Native:
# iOS
npx react-native run-ios

# Android
npx react-native run-android
3

Verify installation

Import the component to verify it’s working:
import Transition from "react-native-screen-transitions";

// This will throw an error if masked-view is not properly installed
<Transition.MaskedView style={{ flex: 1 }}>
  {/* content */}
</Transition.MaskedView>

Complete Implementation

Here’s a full example showing all three parts: source screen, destination screen, and layout configuration.
1

Tag the source element

Create a pressable element with a sharedBoundTag:
// app/index.tsx
import { router } from "expo-router";
import { View } from "react-native";
import Transition from "react-native-screen-transitions";

export default function HomeScreen() {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Transition.Pressable
        sharedBoundTag="album-art"
        style={{
          width: 200,
          height: 200,
          backgroundColor: "#1DB954",
          borderRadius: 12,
        }}
        onPress={() => {
          router.push({
            pathname: "/details",
            params: { sharedBoundTag: "album-art" },
          });
        }}
      />
    </View>
  );
}
The Transition.Pressable measures its bounds on press and stores them with the tag.
2

Wrap destination with MaskedView

Use Transition.MaskedView and match the tag on the destination element:
// app/details.tsx
import { useLocalSearchParams } from "expo-router";
import { Text } from "react-native";
import Transition from "react-native-screen-transitions";

export default function DetailsScreen() {
  const { sharedBoundTag } = useLocalSearchParams<{ sharedBoundTag: string }>();

  return (
    <Transition.MaskedView style={{ flex: 1, backgroundColor: "#121212" }}>
      <Transition.View
        sharedBoundTag={sharedBoundTag}
        style={{
          backgroundColor: "#1DB954",
          width: 400,
          height: 400,
          alignSelf: "center",
          borderRadius: 12,
        }}
      />

      {/* Additional content - will be revealed as mask expands */}
      <Text style={{ color: "white", fontSize: 24, padding: 20 }}>
        Now Playing
      </Text>
    </Transition.MaskedView>
  );
}
The Transition.MaskedView clips content to the animating shared element bounds, creating the reveal effect.
3

Apply the preset in layout

Configure the transition with a preset that uses the shared bounds:
// app/_layout.tsx
import Transition from "react-native-screen-transitions";
import { Stack } from "./stack";

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen
        name="details"
        options={({ route }) => ({
          ...Transition.Presets.SharedAppleMusic({
            sharedBoundTag: route.params?.sharedBoundTag ?? "",
          }),
        })}
      />
    </Stack>
  );
}
The preset interpolates position, size, and mask for a seamless expand/collapse effect.

How It Works

The masked view transition works in four stages:
  1. Source Press: Transition.Pressable measures its layout bounds and stores them with the sharedBoundTag
  2. Navigation: User navigates to the destination screen
  3. Target Registration: Transition.View on the destination registers as the target for that tag
  4. Mask Animation: Transition.MaskedView clips content to animate smoothly between source and target bounds
// Source: 200x200 at position (100, 300)
<Transition.Pressable sharedBoundTag="element" ... />

// During transition: Mask animates from (100,300,200,200) to (0,0,400,400)

// Target: 400x400 at position (0, 0)
<Transition.View sharedBoundTag="element" ... />

Available Presets

These presets require Transition.MaskedView:
Apple Music-style player expansion with elastic mask reveal:
import Transition from "react-native-screen-transitions";

<Stack.Screen
  name="player"
  options={{
    ...Transition.Presets.SharedAppleMusic({
      sharedBoundTag: "album-art",
    }),
  }}
/>
Instagram-style image expansion with mask reveal:
import Transition from "react-native-screen-transitions";

<Stack.Screen
  name="imageDetail"
  options={{
    ...Transition.Presets.SharedIGImage({
      sharedBoundTag: "post-image",
    }),
  }}
/>
SharedXImage preset uses the bounds API but does not require MaskedView. It uses transform-based transitions instead of masking.

Troubleshooting

Make sure you’ve installed @react-native-masked-view/masked-view and rebuilt your app:
# Expo
npx expo install @react-native-masked-view/masked-view
npx expo prebuild --clean
npx expo run:ios

# Bare RN
npm install @react-native-masked-view/masked-view
cd ios && pod install
npx react-native run-ios
Ensure:
  1. Source has sharedBoundTag on Transition.Pressable
  2. Destination has matching sharedBoundTag on Transition.View
  3. Destination content is wrapped in Transition.MaskedView
  4. Layout uses a preset that supports bounds (like SharedAppleMusic)
Check that:
  1. The sharedBoundTag matches between source and destination
  2. The preset is correctly applied in screen options
  3. The source element was pressed (not programmatically navigated without interaction)
This is expected. MaskedView requires native code compilation:
# Create a development build
npx expo prebuild
npx expo run:ios
# or
npx expo run:android