Skip to main content
Masked view is a native module that enables reveal animations like those in Instagram and Apple Music. It’s required for presets like SharedIGImage, SharedAppleMusic, and SharedXImage.

What is Masked View?

A masked view uses one view as a “stencil” to reveal another. Non-transparent areas of the mask are visible; transparent areas hide the content. Example:
  • Mask: A circular image
  • Content: A colored background
  • Result: The background is only visible through the circle

Installation

First, install the native module:
npm install @react-native-masked-view/masked-view

Bare React Native

For bare React Native projects, autolinking handles setup automatically:
npm run android  # or ios
If autolinking fails:
npx react-native link @react-native-masked-view/masked-view

Expo Projects

For Expo projects, you must prebuild (won’t work in Expo Go):
npx expo prebuild --clean
npx expo run:ios   # or android

EAS Build

For managed Expo with EAS Build:
eas build --platform ios --clear-cache
# or
eas build --platform android --clear-cache

Verify Installation

The simplest verification is to render Transition.MaskedView or one of the shared presets on a real device or simulator after installing the native module.

Using Transition.MaskedView

The library provides Transition.MaskedView for integration:
import Transition from "react-native-screen-transitions";
import { View, Image } from "react-native";

function DetailScreen() {
  return (
    <Transition.MaskedView
      maskElement={
        // The mask stencil (opaque = visible, transparent = hidden)
        <View style={{ backgroundColor: "#000", justifyContent: "center", alignItems: "center" }}>
          <Image
            source={{ uri: "..." }}
            style={{ width: 200, height: 200, borderRadius: 100 }}
          />
        </View>
      }
    >
      {/* Content revealed through the mask */}
      <View style={{ flex: 1, backgroundColor: "#007AFF" }} />
    </Transition.MaskedView>
  );
}

Example: Instagram Image Reveal

A shared element animation with masked reveal:
import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
import Transition from "react-native-screen-transitions";
import { View, Text, Image, TouchableOpacity } from "react-native";
import { interpolate } from "react-native-reanimated";

const Stack = createBlankStackNavigator();

// Home screen with image
function HomeScreen({ navigation }) {
  const imageUri = "https://picsum.photos/200";

  return (
    <View style={{ flex: 1, padding: 20, justifyContent: "center" }}>
      <TouchableOpacity
        onPress={() => navigation.navigate("DetailIG", { imageUri })}
      >
        <Transition.Pressable
          sharedBoundTag="ig-image"
          style={{ width: 100, height: 100 }}
        >
          <Image
            source={{ uri: imageUri }}
            style={{ width: 100, height: 100, borderRadius: 8 }}
          />
        </Transition.Pressable>
      </TouchableOpacity>
    </View>
  );
}

// Detail screen with masked reveal
function DetailIGScreen({ route }: any) {
  const { imageUri } = route.params;

  return (
    <View style={{ flex: 1 }}>
      <Transition.MaskedView
        maskElement={
          <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
            <Transition.View
              sharedBoundTag="ig-image"
              style={{ width: 200, height: 200 }}
            >
              <Image
                source={{ uri: imageUri }}
                style={{ width: 200, height: 200, borderRadius: 16 }}
              />
            </Transition.View>
          </View>
        }
      >
        <View style={{ flex: 1, backgroundColor: "#f5f5f5" }}>
          <View style={{ flex: 1, paddingTop: 20, paddingHorizontal: 20 }}>
            <Text style={{ fontSize: 20, fontWeight: "bold" }}>
              Image Details
            </Text>
            <Text style={{ marginTop: 10, color: "#666" }}>
              This image was revealed using a masked animation.
            </Text>
          </View>
        </View>
      </Transition.MaskedView>
    </View>
  );
}

export default function App() {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen
        name="DetailIG"
        component={DetailIGScreen}
        options={{
          screenStyleInterpolator: ({ progress, bounds }) => {
            "worklet";

            const imageTransform = bounds({
              id: "ig-image",
              method: "transform",
            });

            return {
              contentStyle: {
                backgroundColor: interpolate(
                  progress,
                  [0, 1, 2],
                  ["transparent", "#fff", "#fff"]
                ),
              },
              "ig-image": imageTransform,
            };
          },
          gestureEnabled: true,
          gestureDirection: "vertical",
        }}
      />
    </Stack.Navigator>
  );
}

Using Presets

The library includes ready-made masked view presets:

SharedIGImage

Instagram-style image expansion with masked reveal:
<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={{
    ...Transition.Presets.SharedIGImage({
      sharedBoundTag: "ig-image",
    }),
  }}
/>
This handles the mask setup automatically.

SharedAppleMusic

Apple Music album-to-player transition:
<Stack.Screen
  name="Player"
  component={PlayerScreen}
  options={{
    ...Transition.Presets.SharedAppleMusic({
      sharedBoundTag: "album-art",
    }),
  }}
/>

SharedXImage

X/Twitter-style image expansion:
<Stack.Screen
  name="ImageDetail"
  component={ImageDetailScreen}
  options={{
    ...Transition.Presets.SharedXImage({
      sharedBoundTag: "tweet-image",
    }),
  }}
/>
All presets require:
  • sharedBoundTag on source and destination
  • @react-native-masked-view/masked-view native module
  • Bare React Native or Expo prebuild

Custom Masked Animations

Create a custom masked reveal:
const customMaskedAnimation = {
  screenStyleInterpolator: ({ progress, bounds }) => {
    "worklet";

    const imageTransform = bounds({
      id: "photo",
      method: "transform",
    });

    return {
      contentStyle: {
        backgroundColor: interpolate(progress, [0, 1, 2], ["transparent", "#fff", "#fff"]),
      },
      "photo": imageTransform,
    };
  },
};

<Stack.Screen
  name="Detail"
  component={DetailScreen}
  options={customMaskedAnimation}
/>

Performance Considerations

Masked views have performance cost:
  1. Use sparingly — Only on key screens
  2. Limit animations — Avoid multiple simultaneous masks
  3. Simplify content — Keep masked content simple
  4. Test on device — Simulator performance differs

Troubleshooting

”Masked view is not available”

The native module isn’t installed correctly:
# Bare RN
npx react-native doctor
npm run android

# Expo
npx expo prebuild --clean
npx expo run:ios

“Masked view not rendering”

Check that:
  1. maskElement is provided
  2. Content is inside Transition.MaskedView
  3. Native module is linked
  4. Not using Expo Go (won’t work)

Performance issues on Android

Android masked view is more expensive. Consider:
  • Simpler mask shapes
  • Fewer animations
  • Lower resolution on older devices

Next Steps