react-native-reanimated-modal
v1.2.4
Published
A lightweight and performant modal component. Designed for smooth animations, flexibility, and minimal footprint.
Downloads
475
Maintainers
Readme
A lightweight, scalable, flexible, and high-performance modal component. Based on the vanilla Modal component for maximum compatibility and native feel. Built with react-native-reanimated and react-native-gesture-handler.
✨ Features
- 🚀 Performance: Built with react-native-reanimated for 60fps animations that run on the UI thread
- 🎨 Smooth Animations: Supports fade, slide, and scale animations with customizable configs
- 👆 Gesture Support: Interactive swipe-to-dismiss in any direction (up, down, left, right)
- 🪶 Lightweight: Minimal dependencies and smaller bundle size compared to alternatives
- 📱 Native Feel: Uses React Native's Modal component as foundation for platform consistency
- 🔧 Flexible: Highly customizable with extensive prop options
- 📚 TypeScript: Full TypeScript support out of the box
- 🔄 Multi-Modal: Easy integration with React Navigation and support for multiple overlays
🎮 Example
- Install Expo Go on your phone
- Scan the QR code with your camera
- Open the link in Expo Go
- Explore example app!
Or browse the code: 📂 View Example Code →
📚 Documentation
Full API and usage documentation: 🗂️ View Documentation →
📦 Installation
npm install react-native-reanimated-modalyarn add react-native-reanimated-modalpnpm add react-native-reanimated-modalbun add react-native-reanimated-modalRequired Dependencies
This library depends on the following peer dependencies:
- react-native-reanimated (>= 3.0.0)
- react-native-gesture-handler (>= 2.0.0)
- react-native-worklets (>= 0.5.0) - Required for Reanimated 4.0.0+
Note: Make sure to follow the installation guides for all libraries, as they require additional platform-specific setup steps.
🚀 Basic Usage
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Modal } from 'react-native-reanimated-modal';
const App = () => {
const [visible, setVisible] = useState(false);
return (
<GestureHandlerRootView>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Show Modal" onPress={() => setVisible(true)} />
<Modal visible={visible} onHide={() => setVisible(false)}>
<View style={{
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
margin: 20,
}}>
<Text>Hello from Modal!</Text>
<Button title="Close" onPress={() => setVisible(false)} />
</View>
</Modal>
</View>
</GestureHandlerRootView>
);
};📖 API Documentation
New Configuration-Based API (Recommended)
Starting from v1.1.0, we recommend using the new configuration-based API for better type safety and cleaner code:
Animation Configurations
import type { ModalAnimationConfig } from 'react-native-reanimated-modal';
// Scale animation with custom settings
const scaleConfig: ModalAnimationConfig<'scale'> = {
type: 'scale',
duration: 400,
scaleFactor: 0.8, // Start from 80% size
};
// Fade animation
const fadeConfig: ModalAnimationConfig<'fade'> = {
type: 'fade',
duration: 300,
};
// Slide animation with complex directions
const slideConfig: ModalAnimationConfig<'slide'> = {
type: 'slide',
duration: 500,
direction: {
start: 'down', // Slides in from bottom
end: ['down', 'right'], // Can dismiss by swiping down or right
},
};
// Simple slide animation
const simpleSlideConfig: ModalAnimationConfig<'slide'> = {
type: 'slide',
duration: 400,
direction: 'up', // Both slide-in and dismiss direction
};Swipe Configurations
import type { ModalSwipeConfig } from 'react-native-reanimated-modal';
// Basic swipe config
const basicSwipe: ModalSwipeConfig = {
enabled: true,
directions: ['down', 'left', 'right'], // Allow swiping in these directions
threshold: 120,
};
// Advanced swipe config with custom bounce
const advancedSwipe: ModalSwipeConfig = {
enabled: true,
directions: ['up', 'down'], // Only vertical swipes
threshold: 80,
bounceSpringConfig: {
dampingRatio: 0.7,
duration: 400,
},
bounceOpacityThreshold: 0.1,
};
// Disabled swipe
const noSwipe: ModalSwipeConfig = {
enabled: false,
};Usage Examples
<Modal
visible={visible}
animation={scaleConfig}
swipe={advancedSwipe}
>
{/* Your content */}
</Modal>
// Or with inline configs
<Modal
visible={visible}
animation={{
type: 'scale',
duration: 600,
scaleFactor: 0.9,
}}
swipe={{
enabled: true,
threshold: 100,
}}
>
{/* Your content */}
</Modal>
// Backdrop examples
<Modal
visible={visible}
onBackdropPress={false} // Prevent backdrop from closing modal
onHide={() => setVisible(false)} // Only programmatic close allowed
>
{/* Your content */}
</Modal>
<Modal
visible={visible}
backdrop={{ color: 'red', opacity: 0.8 }} // Custom backdrop styling
onBackdropPress={() => console.log('Backdrop pressed!')} // Custom handler
>
{/* Your content */}
</Modal>
// Legacy string syntax still supported
<Modal
visible={visible}
animation="fade" // Equivalent to { type: 'fade', duration: 300 }
>
{/* Your content */}
</Modal>Test IDs
You can pass custom testID props to key elements for easier testing:
| Prop | Type | Default | Description |
|-------------------|----------|---------------------|---------------------------------------------|
| backdropTestID | string | 'modal-backdrop' | testID for the backdrop Pressable |
| contentTestID | string | 'modal-content' | testID for the modal content (Animated.View) |
| containerTestID | string | 'modal-container' | testID for the root container View |
These props are optional and help you write robust e2e/unit tests.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| visible | boolean | false | Controls the visibility of the modal |
| closable | boolean | true | Whether the modal can be closed by user actions |
| children | ReactNode | - | Content to render inside the modal |
| style | StyleProp<ViewStyle> | - | Style for the modal container |
| contentContainerStyle | StyleProp<ViewStyle> | - | Style for the content wrapper |
Configuration Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| animation | ModalAnimationConfigUnion \| ModalAnimation | { type: 'fade', duration: 300 } | Animation configuration object or simple animation type string |
| swipe | ModalSwipeConfig \| false | { enabled: true, directions: ['down'], threshold: 100 } | Swipe gesture configuration |
Backdrop Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| backdrop | ModalBackdropConfig \| ReactNode \| false | { enabled: true, color: 'black', opacity: 0.7 } | Backdrop configuration: false (no backdrop), ReactNode for custom backdrop, or config object |
| onBackdropPress | (() => void) \| false | - | Callback when backdrop is pressed. Set to false to prevent backdrop from closing the modal |
Other Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| coverScreen | boolean | false | If true, covers entire screen without using native Modal |
Event Props
| Prop | Type | Description |
|------|------|-------------|
| onShow | () => void | Called when modal appears |
| onHide | () => void | Called when modal disappears |
React Native Modal Props
The component also accepts these props from React Native's Modal:
hardwareAccelerated(Android)navigationBarTranslucent(Android)statusBarTranslucent(Android)onOrientationChange(iOS)supportedOrientations(iOS)
Constants
The library exports several useful constants for customization:
import {
DEFAULT_MODAL_ANIMATION_DURATION, // 300
DEFAULT_MODAL_SCALE_FACTOR, // 0.8
DEFAULT_MODAL_BACKDROP_CONFIG, // { enabled: true, color: 'black', opacity: 0.7 }
DEFAULT_MODAL_SWIPE_THRESHOLD, // 100
DEFAULT_MODAL_BOUNCE_SPRING_CONFIG, // { dampingRatio: 0.5, duration: 700 }
DEFAULT_MODAL_BOUNCE_OPACITY_THRESHOLD, // 0.05
DEFAULT_MODAL_SWIPE_DIRECTION, // 'down'
} from 'react-native-reanimated-modal';
// Use in your custom configurations
const customAnimationConfig = {
type: 'scale',
duration: DEFAULT_MODAL_ANIMATION_DURATION * 2, // 600ms
scaleFactor: DEFAULT_MODAL_SCALE_FACTOR, // 0.8
};
const customBackdropConfig = {
...DEFAULT_MODAL_BACKDROP_CONFIG,
color: 'rgba(0, 0, 0, 0.8)', // Darker backdrop
opacity: 0.9,
};Types
type SwipeDirection = 'up' | 'down' | 'left' | 'right';
type ModalAnimation = 'fade' | 'slide' | 'scale' | 'custom';
// Animation states for custom worklet functions
type ModalAnimationState =
| 'opening' // Modal is opening (progress: 0 -> 1)
| 'closing' // Modal is closing (progress: 1 -> 0)
| 'sliding' // User is swiping (offsetX, offsetY active)
| 'bouncing' // Bounce back after failed swipe
// Custom worklet function type
type ModalAnimatedStyleFunction = (props: {
animationState: ModalAnimationState | null;
swipeDirection?: SwipeDirection | null; // Active during 'sliding' state
progress: number; // 0-1 during 'opening'/'closing' states
offsetX: number; // Pixel offset during 'sliding' state
offsetY: number; // Pixel offset during 'sliding' state
screenWidth: number; // Device screen width
screenHeight: number; // Device screen height
}) => ViewStyle;
// Configuration Types
type ModalAnimationConfig<T extends ModalAnimation> =
T extends 'fade' ? FadeAnimationConfig :
T extends 'slide' ? SlideAnimationConfig :
T extends 'scale' ? ScaleAnimationConfig :
T extends 'custom' ? CustomAnimationConfig : never;
interface BaseAnimationConfig {
duration?: number;
// Custom worklet functions (optional for all animation types)
contentAnimatedStyle?: ModalAnimatedStyleFunction;
backdropAnimatedStyle?: ModalAnimatedStyleFunction;
}
interface FadeAnimationConfig extends BaseAnimationConfig {
type: 'fade';
}
interface SlideAnimationConfig extends BaseAnimationConfig {
type: 'slide';
direction?: SwipeDirection | {
start: SwipeDirection;
end: SwipeDirection | SwipeDirection[];
};
}
interface ScaleAnimationConfig extends BaseAnimationConfig {
type: 'scale';
scaleFactor?: number; // 0-1, default: 0.8
}
interface CustomAnimationConfig extends BaseAnimationConfig {
type: 'custom';
// No preset animation - relies entirely on contentAnimatedStyle
}
interface ModalSwipeConfig {
enabled?: boolean;
directions?: SwipeDirection[];
threshold?: number;
bounceSpringConfig?: SpringConfig;
bounceOpacityThreshold?: number;
}
interface ModalBackdropConfig {
enabled?: boolean;
color?: string;
opacity?: number;
}
type ModalAnimationConfigUnion =
| FadeAnimationConfig
| SlideAnimationConfig
| ScaleAnimationConfig
| CustomAnimationConfig;🔄 React Navigation support
When using multiple modals simultaneously with @react-navigation/native-stack, you can leverage iOS's FullWindowOverlay for better layering:
import React from 'react';
import { Platform } from 'react-native';
import { FullWindowOverlay } from 'react-native-screens';
import { Modal } from 'react-native-reanimated-modal';
const isIOS = Platform.OS === 'ios';
const withOverlay = (element: React.ReactNode) =>
isIOS ? <FullWindowOverlay>{element}</FullWindowOverlay> : element;
const MultiModalExample = () => {
const [firstModalVisible, setFirstModalVisible] = useState(false);
const [secondModalVisible, setSecondModalVisible] = useState(false);
return withOverlay(
<>
<Modal
visible={firstModalVisible}
coverScreen // Important: excludes native Modal usage
onHide={() => setFirstModalVisible(false)}
>
{/* First modal content */}
</Modal>
<Modal
visible={secondModalVisible}
coverScreen // Important: excludes native Modal usage
onHide={() => setSecondModalVisible(false)}
>
{/* Second modal content */}
</Modal>
</>
);
};Important: When using multiple modals with
FullWindowOverlay, always setcoverScreen={true}prop to exclude the usage of React Native's native Modal component and ensure proper layering.
🎨 Advanced Examples
Fade Animation with Custom Duration
<Modal
visible={visible}
animation={{
type: 'fade',
duration: 400,
}}
swipe={{
directions: ['down', 'right'],
threshold: 100,
}}
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>Scale Animation with Custom Duration
<Modal
visible={visible}
animation={{
type: 'scale',
duration: 400,
scaleFactor: 0.8,
}}
swipe={{
directions: ['down', 'right'],
threshold: 100,
}}
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>Custom Slide Animation with Swipe Directions
<Modal
visible={visible}
animation={{
type: 'slide',
duration: 500,
direction: {
start: 'down', // Slides in from bottom
end: ['down', 'right'], // Can dismiss by swiping down or right
},
}}
swipe={{
threshold: 150,
bounceSpringConfig: {
dampingRatio: 0.8,
duration: 400,
},
}}
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>Note: When using slide animation with complex directions, the
startproperty determines the initial slide-in direction, while theendproperty (array or single direction) defines the available swipe-to-dismiss directions.
Full Screen Modal
<Modal
visible={visible}
contentContainerStyle={{ flex: 1 }}
animation={{
type: 'slide',
duration: 300,
direction: 'down',
}}
swipe={{
directions: ['down'],
threshold: 80,
}}
backdrop={false} // No backdrop for full screen
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>Custom Animation Worklets
You can create fully custom animations by providing worklet functions that run on the UI thread. These functions receive animation state information and return style objects that get merged with the default preset animations.
// Custom slide animation with rotation and color changes
<Modal
visible={visible}
animation={{
type: 'slide',
duration: 500,
direction: 'down',
contentAnimatedStyle: ({ progress, animationState }) => {
'worklet';
return {
// Add rotation during slide animation
transform: [{ rotate: `${progress * 360}deg` }],
// Change background color based on progress
backgroundColor: `rgba(255, 0, 0, ${progress * 0.5})`,
};
},
backdropAnimatedStyle: ({ progress }) => {
'worklet';
return {
// Custom backdrop with color transition
backgroundColor: `rgba(0, 255, 0, ${progress * 0.3})`,
};
}
}}
>
{/* Modal content */}
</Modal>
// Custom animation type without any presets
<Modal
visible={visible}
animation={{
type: 'custom',
duration: 800,
contentAnimatedStyle: ({ progress, offsetX, offsetY, animationState }) => {
'worklet';
if (animationState === 'sliding') {
// During swipe gestures
return {
opacity: 1 - Math.abs(offsetX) / 200,
transform: [
{ translateX: offsetX },
{ scale: 1 - Math.abs(offsetX) / 400 }
],
};
}
// During open/close animations
return {
opacity: progress,
transform: [
{ scale: 0.5 + progress * 0.5 },
{ rotateY: `${(1 - progress) * 180}deg` }
],
};
}
}}
>
{/* Modal content */}
</Modal>
// Enhance existing animations with custom effects
<Modal
visible={visible}
animation={{
type: 'fade', // Use fade preset
duration: 600,
contentAnimatedStyle: ({ progress }) => {
'worklet';
// Add elastic scale effect to fade animation
const elasticScale = 1 + Math.sin(progress * Math.PI * 2) * 0.1;
return {
transform: [{ scale: elasticScale }]
};
}
}}
>
{/* Modal content */}
</Modal>Custom Backdrop Examples
// Disable backdrop completely
<Modal visible={visible} backdrop={false}>
{/* Modal content */}
</Modal>
// Custom backdrop configuration
<Modal
visible={visible}
backdrop={{
enabled: true,
color: 'rgba(255, 0, 0, 0.3)',
opacity: 0.8
}}
>
{/* Modal content */}
</Modal>
// Custom backdrop renderer (e.g., BlurView)
<Modal
visible={visible}
backdrop={
<BlurView
style={StyleSheet.absoluteFill}
blurType="light"
blurAmount={10}
/>
}
>
{/* Modal content */}
</Modal>🤝 Contributing
See the contributing guide to learn how to contribute to the repository and the development workflow.
📄 License
MIT
Made with create-react-native-library
