UNPKG

react-native-reanimated-modal

Version:

A lightweight and performant modal component. Designed for smooth animations, flexibility, and minimal footprint.

657 lines (540 loc) â€Ē 18.9 kB
[![documentation](https://img.shields.io/badge/documentation-blue.svg)](https://whidrubeld.github.io/react-native-reanimated-modal/) [![npm bundle size](https://img.shields.io/bundlephobia/min/react-native-reanimated-modal)](https://www.npmjs.com/package/react-native-reanimated-modal) A lightweight, scalable, flexible, and high-performance modal component. Based on the vanilla [Modal](https://reactnative.dev/docs/modal) component for maximum compatibility and native feel. Built with [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated/) and [react-native-gesture-handler](https://github.com/software-mansion/react-native-gesture-handler). <img src="https://github.com/whidrubeld/react-native-reanimated-modal/blob/main/.github/images/example.gif?raw=true" width="100%" style="max-width: 500px; height: auto;" alt="React Native Reanimated Modal Demo" /> ## âœĻ 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 <img src="https://github.com/whidrubeld/react-native-reanimated-modal/blob/main/.github/images/eas-update.svg?raw=true" alt="Expo QR Code" width="200" height="200" /> 1. Install [Expo Go](https://expo.dev/client) 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 →**](https://github.com/whidrubeld/react-native-reanimated-modal/tree/main/example) ## 📚 Documentation **Full API and usage documentation**: [**🗂ïļ View Documentation →**](https://whidrubeld.github.io/react-native-reanimated-modal/) ## ðŸ“Ķ Installation ```sh npm install react-native-reanimated-modal ``` ```sh yarn add react-native-reanimated-modal ``` ```sh pnpm add react-native-reanimated-modal ``` ```sh bun add react-native-reanimated-modal ``` ### Required Dependencies This library depends on the following peer dependencies: 1. **[react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/)** (>= 3.0.0) 2. **[react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation)** (>= 2.0.0) 3. **[react-native-worklets](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/#dependencies)** (>= 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 ```tsx 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 ```tsx 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 ```tsx 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 ```tsx <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: ```tsx 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 ```tsx 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: ```tsx 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 set `coverScreen={true}` prop to exclude the usage of React Native's native Modal component and ensure proper layering. ## ðŸŽĻ Advanced Examples ### Fade Animation with Custom Duration ```tsx <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 ```tsx <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 ```tsx <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 `start` property determines the initial slide-in direction, while the `end` property (array or single direction) defines the available swipe-to-dismiss directions. ### Full Screen Modal ```tsx <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. ```tsx // 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 ```tsx // 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](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. ## 📄 License MIT --- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)