Deprecation Notice
Transition.MaskedView is deprecated in v3.4. For new code, use the built-in navigationMaskEnabled option instead. This guide documents the legacy approach for existing projects.
Using navigationMaskEnabled (Recommended)
The new approach is simpler and requires no extra components:
import { Transition } from 'react-native-screen-transitions';
<Transition.Stack
navigationMaskEnabled // Built-in masking
screenStyleInterpolator={screenStyleInterpolator}
gestureEnabled
>
{/* screens */}
</Transition.Stack>
This automatically provides masking for shared element reveal effects without manual setup.
Legacy: Transition.MaskedView
The old approach wraps the navigator with Transition.MaskedView:
// ❌ Deprecated approach
<Transition.MaskedView>
<Transition.Stack
screenStyleInterpolator={screenStyleInterpolator}
>
{/* screens */}
</Transition.Stack>
</Transition.MaskedView>
This still works but is not recommended for new projects.
What is Masking?
Masking clips content to prevent visual overflow during shared element animations. Without masking, expanding elements may bleed outside their natural bounds.
With masking enabled:
- Shared elements clip properly during expansion
- Reveal animations feel smooth and contained
- Content doesn’t visually overflow during transitions
When You Need Masking
Shared Element Expansion
When animating an element from small (thumbnail) to large (fullscreen):
// Without masking: Element edges blur/clip badly
// With masking: Clean, contained expansion
<Transition.Boundary.Pressable id="hero" onPress={navigate}>
<Image style={{ width: 80, height: 80, borderRadius: 8 }} />
</Transition.Boundary.Pressable>
<Transition.Boundary.View id="hero">
<Image style={{ width: 300, height: 400, borderRadius: 0 }} />
</Transition.Boundary.View>
Shape-Changing Elements
When border radius or dimensions change:
// Expanding from rounded thumbnail to fullscreen
// Masking ensures the border radius transition looks smooth
<Transition.Stack navigationMaskEnabled>
{/* screens with rounded thumbnail → fullscreen expansion */}
</Transition.Stack>
Reveal Effects
Progressively revealing content:
const revealInterpolator = ({ progress }) => {
"worklet";
return {
content: {
style: {
borderRadius: interpolate(progress, [0, 1], [8, 0]),
},
},
};
};
With masking, the reveal is clean and contained.
Masking adds minor GPU overhead:
- Modern devices: Negligible impact
- Low-end devices: 1–3% performance cost
- Conditional masking: Only enable when needed
Use navigationMaskEnabled selectively:
// Enable masking only for shared element stack
<Transition.Stack
navigationMaskEnabled // Only when shared elements present
screenStyleInterpolator={sharedElementInterpolator}
>
{/* screens */}
</Transition.Stack>
// Standard stack without shared elements—no masking
<Transition.Stack
screenStyleInterpolator={simpleInterpolator}
>
{/* screens */}
</Transition.Stack>
Legacy Implementation Details
If upgrading from old MaskedView usage, understand how it worked:
Masked View Hierarchy
<Transition.MaskedView>
<Transition.Stack>
{/* Screens rendered here, clipped by mask */}
</Transition.Stack>
</Transition.MaskedView>
The MaskedView acted as a clipping boundary, preventing content from overflowing during animations.
Mask Shape
The mask shape followed the animation bounds:
// Mask adapts to expanding element bounds
// As element grows, mask expands with it
This required Reanimated 2/3 internals and was complex to maintain.
Why Deprecated
- Complex implementation: Required low-level mask management
- Platform differences: iOS and Android had different behavior
- Better alternative:
navigationMaskEnabled is simpler and more reliable
- Performance: Built-in masking is optimized at the native level
Migration Path
Before (Old MaskedView)
// ❌ Old code
import { Transition } from 'react-native-screen-transitions';
<Transition.MaskedView>
<Transition.Stack
screenStyleInterpolator={({ progress, bounds }) => ({
content: {
style: {
borderRadius: interpolate(progress, [0, 1], [8, 0]),
},
},
})}
>
{/* screens */}
</Transition.Stack>
</Transition.MaskedView>
After (New navigationMaskEnabled)
// ✅ New code
import { Transition } from 'react-native-screen-transitions';
<Transition.Stack
navigationMaskEnabled
screenStyleInterpolator={({ progress, bounds }) => ({
content: {
style: {
borderRadius: interpolate(progress, [0, 1], [8, 0]),
},
},
})}
>
{/* screens */}
</Transition.Stack>
Just remove MaskedView wrapper and add navigationMaskEnabled prop.
Testing Masking Behavior
Verify masking is working by checking visual overflow:
// Test: Expanding shared element
const testInterpolator = ({ progress, bounds }) => {
"worklet";
const b = bounds({ id: "test" });
return {
content: {
style: {
// Element expands significantly
transform: [
{ scale: interpolate(progress, [0, 1], [0.1, 2]) },
],
},
},
};
};
<Transition.Stack
navigationMaskEnabled
screenStyleInterpolator={testInterpolator}
>
{/* screens with expanding element */}
</Transition.Stack>
Without masking, the expanded element would overflow. With masking, it’s contained.
Visual Debugging
Enable debug mode to see mask boundaries:
// During development, visualize mask with debug style
const debugInterpolator = ({ progress, bounds }) => {
"worklet";
return {
content: {
style: {
opacity: interpolate(progress, [0, 1], [0, 1]),
// Add debug border to see bounds
borderWidth: __DEV__ ? 1 : 0,
borderColor: __DEV__ ? 'red' : 'transparent',
},
},
};
};
Edge Cases
Overflow: Hidden
Ensure parents don’t have conflicting overflow settings:
// ✅ Let masking handle clipping
<Transition.Stack navigationMaskEnabled>
{/* No overflow: hidden on parents */}
</Transition.Stack>
// ⚠️ May conflict
<View style={{ overflow: 'hidden' }}>
<Transition.Stack navigationMaskEnabled>
{/* Multiple clipping layers */}
</Transition.Stack>
</View>
Shared element transforms should use consistent origins:
// ✅ Consistent origin
transform: [
{ translateX: 0 },
{ scale: 1 },
]
// ⚠️ May clip unexpectedly
transform: [
{ translateX: 100 },
{ scale: 2 },
{ translateX: -50 },
]
Nested Masked Stacks
Avoid nesting multiple masked stacks:
// ⚠️ Avoid nesting
<Transition.Stack navigationMaskEnabled>
<Transition.Modal navigationMaskEnabled>
{/* Multiple mask layers */}
</Transition.Modal>
</Transition.Stack>
// ✅ Better: Only outermost stack
<Transition.Stack navigationMaskEnabled>
<Transition.Modal>
{/* Inherits parent mask */}
</Transition.Modal>
</Transition.Stack>
Troubleshooting
Content Clipping Unexpectedly
Check if masking is enabled:
// Verify navigationMaskEnabled is set
<Transition.Stack navigationMaskEnabled>
{/* Should not clip unexpectedly */}
</Transition.Stack>
Reveal Animation Not Smooth
Ensure interpolator uses smooth easing:
// ❌ Jagged
borderRadius: interpolate(progress, [0, 0.5, 1], [8, 0, 0])
// ✅ Smooth
borderRadius: interpolate(progress, [0, 1], [8, 0])
Masking Disabled on Older Devices
navigationMaskEnabled works on all devices. If experiencing issues:
- Upgrade React Native to 0.73+
- Upgrade Reanimated to 3.0+
- Test on actual device (simulator may have issues)
Best Practices
- Use
navigationMaskEnabled in modern code
- Only enable when needed (shared elements present)
- Test on low-end devices for performance
- Use smooth interpolations in animations
- Avoid nested masks in complex hierarchies
- Monitor performance with Profiler
Alternatives to Masking
If masking causes issues, consider:
Option 1: Simple Opacity
const simpleInterpolator = ({ progress }) => {
"worklet";
return {
content: {
style: {
opacity: interpolate(progress, [0, 1], [0, 1]),
},
},
};
};
No shape changes, so no masking needed.
Option 2: Smaller Expansion
// Expand 1x → 1.5x instead of 1x → 2x
const limitedInterpolator = ({ progress }) => {
"worklet";
return {
content: {
style: {
transform: [
{ scale: interpolate(progress, [0, 1], [0.8, 1.5]) },
],
},
},
};
};
Smaller expansion may not need masking.
Option 3: Pre-render Masked Assets
Prepare assets with appropriate bounds pre-rendered:
// Instead of animating shape, animate pre-rendered assets
<Image source={thumbnailImage} /> // Thumbnail
// Transition to
<Image source={expandedImage} /> // Expanded version pre-rendered
Summary
- New code: Use
navigationMaskEnabled
- Legacy code:
Transition.MaskedView still works but deprecated
- Migration: Remove
MaskedView, add navigationMaskEnabled
- Performance: Negligible cost on modern devices
- Best practices: Enable only when needed, use smooth animations
For most use cases, navigationMaskEnabled provides perfect results with zero configuration.
Next Steps