Project React Native Reanimated Modal

Expand description

documentation npm bundle size

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.

React Native Reanimated Modal Demo
  • 🚀 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
Expo QR Code
  1. Install Expo Go on your phone
  2. Scan the QR code with your camera
  3. Open the link in Expo Go
  4. Explore example app!

Or browse the code: 📂 View Example Code →

Full API and usage documentation: 🗂️ View Documentation →

npm install react-native-reanimated-modal
yarn add react-native-reanimated-modal
pnpm add react-native-reanimated-modal
bun add react-native-reanimated-modal

This library depends on the following peer dependencies:

  1. react-native-reanimated (>= 3.0.0)
  2. react-native-gesture-handler (>= 2.0.0)

Note: Make sure to follow the installation guides for both libraries, as they require additional platform-specific setup steps.

Make sure to wrap your root App component with gestureHandlerRootHOC for gesture handling to work properly:

import { gestureHandlerRootHOC } from 'react-native-gesture-handler';

const App = () => {
{/* Your app */}
};

export default gestureHandlerRootHOC(App);
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { Modal } from 'react-native-reanimated-modal';

const App = () => {
const [visible, setVisible] = useState(false);

return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Show Modal" onPress={() => setVisible(true)} />

<Modal
visible={visible}
onHide={() => setVisible(false)}
animationConfig={{
animation: 'scale',
duration: 400,
scaleFactor: 0.8,
}}
swipeConfig={{
enabled: true,
directions: ['down', 'left', 'right'],
threshold: 100,
}}
>
<View style={{
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
margin: 20
}}>
<Text>Hello from Modal!</Text>
<Button title="Close" onPress={() => setVisible(false)} />
</View>
</Modal>
</View>
);
};

export default gestureHandlerRootHOC(App);

Starting from v1.1.0, we recommend using the new configuration-based API for better type safety and cleaner code:

import type { ModalAnimationConfig } from 'react-native-reanimated-modal';

// Scale animation with custom settings
const scaleConfig: ModalAnimationConfig<'scale'> = {
animation: 'scale',
duration: 400,
scaleFactor: 0.8, // Start from 80% size
};

// Fade animation
const fadeConfig: ModalAnimationConfig<'fade'> = {
animation: 'fade',
duration: 300,
};

// Slide animation with complex directions
const slideConfig: ModalAnimationConfig<'slide'> = {
animation: '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'> = {
animation: 'slide',
duration: 400,
direction: 'up', // Both slide-in and dismiss direction
};
import type { SwipeConfig } from 'react-native-reanimated-modal';

// Basic swipe config
const basicSwipe: SwipeConfig = {
enabled: true,
directions: ['down', 'left', 'right'], // Allow swiping in these directions
threshold: 120,
};

// Advanced swipe config with custom bounce
const advancedSwipe: SwipeConfig = {
enabled: true,
directions: ['up', 'down'], // Only vertical swipes
threshold: 80,
bounceSpringConfig: {
stiffness: 300,
dampingRatio: 0.7,
duration: 400,
},
bounceOpacityThreshold: 0.1,
};

// Disabled swipe
const noSwipe: SwipeConfig = {
enabled: false,
};
<Modal
visible={visible}
animationConfig={scaleConfig}
swipeConfig={advancedSwipe}
>
{/* Your content */}
</Modal>

// Or with inline configs
<Modal
visible={visible}
animationConfig={{
animation: 'scale',
duration: 600,
scaleFactor: 0.9,
}}
swipeConfig={{
enabled: true,
threshold: 100,
}}
>
{/* Your content */}
</Modal>

// Legacy string syntax still supported
<Modal
visible={visible}
animationConfig="fade" // Equivalent to { animation: 'fade', duration: 300 }
>
{/* Your content */}
</Modal>

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.

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
renderBackdrop () => ReactNode - Custom backdrop renderer
Prop Type Default Description
animationConfig ModalAnimationConfigUnion | ModalAnimation { animation: 'fade', duration: 300 } Animation configuration object or simple animation type string
swipeConfig SwipeConfig { enabled: true, directions: ['down'], threshold: 100 } Swipe gesture configuration
Prop Type Default Description
hasBackdrop boolean true Whether to show backdrop behind modal
backdropColor string 'black' Color of the backdrop
backdropOpacity number 0.7 Opacity of the backdrop (0-1)
onBackdropPress () => void - Callback when backdrop is pressed
Prop Type Default Description
coverScreen boolean false If true, covers entire screen without using native Modal
Prop Type Description
onShow () => void Called when modal appears
onHide () => void Called when modal disappears

The component also accepts these props from React Native's Modal:

  • hardwareAccelerated (Android)
  • navigationBarTranslucent (Android)
  • statusBarTranslucent (Android)
  • onOrientationChange (iOS)
  • supportedOrientations (iOS)

The library exports several useful constants for customization:

import {
DEFAULT_MODAL_ANIMATION_DURATION, // 300
DEFAULT_MODAL_SCALE_FACTOR, // 0.8
DEFAULT_MODAL_BACKDROP_OPACITY, // 0.7
DEFAULT_MODAL_BACKDROP_COLOR, // 'black'
DEFAULT_MODAL_SWIPE_THRESHOLD, // 100
DEFAULT_MODAL_BOUNCE_SPRING_CONFIG, // { stiffness: 200, 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 = {
animation: 'scale',
duration: DEFAULT_MODAL_ANIMATION_DURATION * 2, // 600ms
scaleFactor: DEFAULT_MODAL_SCALE_FACTOR, // 0.8
};
type SwipeDirection = 'up' | 'down' | 'left' | 'right';
type ModalAnimation = 'fade' | 'slide' | 'scale';

// New Configuration Types
type ModalAnimationConfig<T extends ModalAnimation> =
T extends 'fade' ? FadeAnimationConfig :
T extends 'slide' ? SlideAnimationConfig :
T extends 'scale' ? ScaleAnimationConfig : never;

interface FadeAnimationConfig {
animation: 'fade';
duration?: number;
}

interface SlideAnimationConfig {
animation: 'slide';
duration?: number;
direction?: SwipeDirection | {
start: SwipeDirection;
end: SwipeDirection | SwipeDirection[];
};
}

interface ScaleAnimationConfig {
animation: 'scale';
duration?: number;
scaleFactor?: number; // 0-1, default: 0.8
}

interface SwipeConfig {
enabled?: boolean;
threshold?: number;
bounceSpringConfig?: SpringConfig;
bounceOpacityThreshold?: number;
}

type ModalAnimationConfigUnion =
| FadeAnimationConfig
| SlideAnimationConfig
| ScaleAnimationConfig;

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
onBackdropPress={() => setFirstModalVisible(false)}
>
{/* First modal content */}
</Modal>

<Modal
visible={secondModalVisible}
coverScreen // Important: excludes native Modal usage
onBackdropPress={() => setSecondModalVisible(false)}
>
{/* Second modal content */}
</Modal>
</>
);
};

Important: When using multiple modals with FullWindowOverlay, always set coverScreen={true} prop to exclude the usage of React Native's native Modal component and ensure proper layering.

<Modal
visible={visible}
animationConfig={{
animation: 'fade',
duration: 400,
}}
swipeConfig={{
directions: ['down', 'right'],
threshold: 100,
}}
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>
<Modal
visible={visible}
animationConfig={{
animation: 'scale',
duration: 400,
scaleFactor: 0.8,
}}
swipeConfig={{
directions: ['down', 'right'],
threshold: 100,
}}
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>
<Modal
visible={visible}
animationConfig={{
animation: 'slide',
duration: 500,
direction: {
start: 'down', // Slides in from bottom
end: ['down', 'right'], // Can dismiss by swiping down or right
},
}}
swipeConfig={{
threshold: 150,
bounceSpringConfig: {
stiffness: 300,
dampingRatio: 0.8,
duration: 400,
},
}}
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>

Note: When using slide animation with complex directions, the start property determines the initial slide-in direction, while the end property (array or single direction) defines the available swipe-to-dismiss directions.

<Modal
visible={visible}
contentContainerStyle={{ flex: 1 }}
animationConfig={{
animation: 'slide',
duration: 300,
direction: 'down',
}}
swipeConfig={{
directions: ['down'],
threshold: 80,
}}
hasBackdrop={false} // No backdrop for full screen
onHide={() => setVisible(false)}
>
{/* Modal content */}
</Modal>

See the contributing guide to learn how to contribute to the repository and the development workflow.

MIT


Made with create-react-native-library

Type Aliases§

Source§

type ModalAnimation = "fade" | "slide" | "scale"

Source§

type ModalAnimationConfig<T> = T extends "fade"
    ? FadeAnimationConfig
    : T extends "slide"
        ? SlideAnimationConfig
        : T extends "scale" ? ScaleAnimationConfig : never

Generic type for animation config based on animation type.

Source§

type ModalAnimationConfigUnion = FadeAnimationConfig | SlideAnimationConfig | ScaleAnimationConfig

Union of all animation configuration types.

Source§

type SwipeDirection = "up" | "down" | "left" | "right"

Variables§

Source§

const DEFAULT_MODAL_ANIMATION_CONFIGS: {
    fade: FadeAnimationConfig;
    scale: ScaleAnimationConfig;
    slide: SlideAnimationConfig;
} = ...

Default animation configurations.

Source§

const DEFAULT_MODAL_ANIMATION_DURATION: 300 = [object Object]

Default values and configurations.

Source§

const DEFAULT_MODAL_BACKDROP_COLOR: "black" = 'black'

Source§

const DEFAULT_MODAL_BACKDROP_OPACITY: 0.7 = [object Object]

Source§

const DEFAULT_MODAL_BOUNCE_OPACITY_THRESHOLD: 0.05 = [object Object]

Source§

const DEFAULT_MODAL_BOUNCE_SPRING_CONFIG: { dampingRatio: 0.5; duration: 700; stiffness: 200 } = ...

Source§

const DEFAULT_MODAL_SCALE_FACTOR: 0.8 = [object Object]

Source§

const DEFAULT_MODAL_SWIPE_CONFIG: SwipeConfig = ...

Default swipe configuration.

Source§

const DEFAULT_MODAL_SWIPE_DIRECTION: SwipeDirection = 'down'

Source§

const DEFAULT_MODAL_SWIPE_THRESHOLD: 100 = [object Object]

Source§

const Modal: FC<ModalProps> = ...

Modal component with smooth, customizable animations and gesture support. Built on top of React Native's Modal, Reanimated, and Gesture Handler.

props

Props for the modal component.

returns

Functions§

Source§

getSlideInDirection(
    animationConfig: ModalAnimationConfigUnion,
    fallback?: SwipeDirection,
): SwipeDirection

Gets the slide-in direction from animation config.

Source§

getSwipeDirections(
    swipeConfig: SwipeConfig,
    animationConfig?: ModalAnimationConfigUnion,
    fallback?: SwipeDirection | SwipeDirection[],
): SwipeDirection[]

Extracts swipe directions from swipe config or animation config fallback.

Source§

normalizeAnimationConfig(
    config?: undefined | ModalAnimation | Partial<ModalAnimationConfigUnion>,
): ModalAnimationConfigUnion

Normalizes animation configuration by providing defaults for missing properties.

Source§

normalizeSwipeConfig(config?: Partial<SwipeConfig>): SwipeConfig

Normalizes swipe configuration by providing defaults for missing properties.

Interfaces§

FadeAnimationConfig

Configuration for fade animation.

ModalProps

Props for the Modal component.

ScaleAnimationConfig

Configuration for scale animation.

SlideAnimationConfig

Configuration for slide animation.

SwipeConfig

Configuration for swipe gestures.