An enhanced React Native modal component with animations, customizable backdrop, and swipe-to-dismiss functionality
npx @tessl/cli install tessl/npm-react-native-modal@13.0.0React Native Modal is an enhanced, animated, and highly customizable modal component for React Native applications. It extends the original React Native Modal component by adding smooth enter/exit animations, customizable backdrop appearance, swipe-to-dismiss functionality, and comprehensive event handling with device rotation support and keyboard avoidance.
npm install react-native-modal or yarn add react-native-modalimport Modal from "react-native-modal";For named imports:
import {
ReactNativeModal,
ModalProps,
OnSwipeCompleteParams,
AnimationEvent,
Animations,
SupportedAnimation,
Orientation,
Direction,
PresentationStyle,
OnOrientationChange,
GestureResponderEvent
} from "react-native-modal";CommonJS:
const Modal = require("react-native-modal");For TypeScript usage with React Native types:
import { StyleProp, ViewStyle, NativeSyntheticEvent, NativeTouchEvent } from "react-native";
import { PanResponderGestureState } from "react-native";import React, { useState } from "react";
import { Button, Text, View } from "react-native";
import Modal from "react-native-modal";
function ModalExample() {
const [isModalVisible, setModalVisible] = useState(false);
const toggleModal = () => {
setModalVisible(!isModalVisible);
};
return (
<View style={{ flex: 1 }}>
<Button title="Show modal" onPress={toggleModal} />
<Modal isVisible={isModalVisible}>
<View style={{ flex: 1, backgroundColor: "white" }}>
<Text>Hello! I am a modal.</Text>
<Button title="Hide modal" onPress={toggleModal} />
</View>
</Modal>
</View>
);
}React Native Modal is built around several key components:
The main modal component providing enhanced functionality over React Native's built-in Modal.
/**
* Enhanced React Native modal component with animations and gesture support
*/
class ReactNativeModal extends React.Component<ModalProps, State> {
static defaultProps: typeof defaultProps;
}
/**
* Default export - same as ReactNativeModal class
*/
export default ReactNativeModal;Complete props interface for configuring modal behavior and appearance.
type ModalProps = ViewProps & {
children: React.ReactNode;
// Swipe Gesture Props
onSwipeStart?: (gestureState: PanResponderGestureState) => void;
onSwipeMove?: (percentageShown: number, gestureState: PanResponderGestureState) => void;
onSwipeComplete?: (params: OnSwipeCompleteParams, gestureState: PanResponderGestureState) => void;
onSwipeCancel?: (gestureState: PanResponderGestureState) => void;
style?: StyleProp<ViewStyle>;
swipeDirection?: Direction | Array<Direction>;
// Inherited React Native Modal Props
onDismiss?: () => void;
onShow?: () => void;
hardwareAccelerated?: boolean;
onOrientationChange?: OnOrientationChange;
presentationStyle?: PresentationStyle;
// Additional Modal Props (defaults provided by defaultProps)
useNativeDriverForBackdrop?: boolean;
} & typeof defaultProps;
// All properties from defaultProps are automatically available as optional props:
// animationIn, animationInTiming, animationOut, animationOutTiming, avoidKeyboard,
// coverScreen, hasBackdrop, backdropColor, backdropOpacity, backdropTransitionInTiming,
// backdropTransitionOutTiming, customBackdrop, useNativeDriver, deviceHeight, deviceWidth,
// hideModalContentWhileAnimating, propagateSwipe, isVisible, panResponderThreshold,
// swipeThreshold, onModalShow, onModalWillShow, onModalHide, onModalWillHide,
// onBackdropPress, onBackButtonPress, scrollTo, scrollOffset, scrollOffsetMax,
// scrollHorizontal, statusBarTranslucent, supportedOrientationsParameters passed to the onSwipeComplete callback when a swipe gesture completes.
interface OnSwipeCompleteParams {
swipingDirection: Direction;
}/**
* Initializes custom slide animations, overriding react-native-animatable defaults
*/
function initializeAnimations(): void;
/**
* Builds animation configuration objects, handling custom animation definitions
*/
function buildAnimations(options: {
animationIn: Animation | CustomAnimation;
animationOut: Animation | CustomAnimation;
}): Animations;
/**
* Utility function for animation calculations: returns -(x - 1)
*/
function reversePercentage(x: number): number;
/**
* Creates slide translation animation objects for react-native-animatable
*/
function makeSlideTranslation(
translationType: string,
fromValue: number,
toValue: number
): CustomAnimation;type SupportedAnimation = Animation | CustomAnimation;
interface Animations {
animationIn: string;
animationOut: string;
}
type Orientation =
| "portrait"
| "portrait-upside-down"
| "landscape"
| "landscape-left"
| "landscape-right";
type Direction = "up" | "down" | "left" | "right";
type PresentationStyle =
| "fullScreen"
| "pageSheet"
| "formSheet"
| "overFullScreen";
type AnimationEvent = (...args: any[]) => void;
type OnOrientationChange = (orientation: NativeSyntheticEvent<any>) => void;
type OrNull<T> = null | T;
interface GestureResponderEvent extends NativeSyntheticEvent<NativeTouchEvent> {}
interface PanResponderGestureState {
stateID: number;
moveX: number;
moveY: number;
x0: number;
y0: number;
dx: number;
dy: number;
vx: number;
vy: number;
numberActiveTouches: number;
}
// React Native types used in the API
type StyleProp<T> = T | T[] | null | undefined;
type ViewStyle = Record<string, any>;
interface ViewProps {
style?: StyleProp<ViewStyle>;
testID?: string;
accessible?: boolean;
accessibilityLabel?: string;
accessibilityHint?: string;
accessibilityRole?: string;
accessibilityState?: Record<string, any>;
accessibilityValue?: Record<string, any>;
onLayout?: (event: any) => void;
pointerEvents?: 'none' | 'box-none' | 'box-only' | 'auto';
removeClippedSubviews?: boolean;
renderToHardwareTextureAndroid?: boolean;
shouldRasterizeIOS?: boolean;
collapsable?: boolean;
needsOffscreenAlphaCompositing?: boolean;
onStartShouldSetResponder?: (event: any) => boolean;
onMoveShouldSetResponder?: (event: any) => boolean;
onResponderGrant?: (event: any) => void;
onResponderMove?: (event: any) => void;
onResponderRelease?: (event: any) => void;
onResponderTerminate?: (event: any) => void;
onResponderTerminationRequest?: (event: any) => boolean;
onStartShouldSetResponderCapture?: (event: any) => boolean;
onMoveShouldSetResponderCapture?: (event: any) => boolean;
}
interface NativeSyntheticEvent<T> {
nativeEvent: T;
currentTarget: number;
target: number;
bubbles?: boolean;
cancelable?: boolean;
defaultPrevented?: boolean;
eventPhase?: number;
isTrusted?: boolean;
preventDefault(): void;
stopPropagation(): void;
persist(): void;
timeStamp: number;
type: string;
}
interface NativeTouchEvent {
changedTouches: Array<NativeTouchEvent>;
identifier: string;
locationX: number;
locationY: number;
pageX: number;
pageY: number;
target: string;
timestamp: number;
touches: Array<NativeTouchEvent>;
}const defaultProps = {
animationIn: "slideInUp" as Animation | CustomAnimation,
animationInTiming: 300,
animationOut: "slideOutDown" as Animation | CustomAnimation,
animationOutTiming: 300,
avoidKeyboard: false,
coverScreen: true,
hasBackdrop: true,
backdropColor: "black",
backdropOpacity: 0.7,
backdropTransitionInTiming: 300,
backdropTransitionOutTiming: 300,
customBackdrop: null,
useNativeDriver: false,
deviceHeight: null,
deviceWidth: null,
hideModalContentWhileAnimating: false,
propagateSwipe: false,
isVisible: false,
panResponderThreshold: 4,
swipeThreshold: 100,
onModalShow: () => null,
onModalWillShow: () => null,
onModalHide: () => null,
onModalWillHide: () => null,
onBackdropPress: () => null,
onBackButtonPress: () => null,
scrollTo: null,
scrollOffset: 0,
scrollOffsetMax: 0,
scrollHorizontal: false,
statusBarTranslucent: false,
supportedOrientations: ["portrait", "landscape"] as Orientation[]
};import React, { useState } from "react";
import { Button, Text, View } from "react-native";
import Modal from "react-native-modal";
function AnimatedModal() {
const [isVisible, setVisible] = useState(false);
return (
<View style={{ flex: 1 }}>
<Button title="Show Modal" onPress={() => setVisible(true)} />
<Modal
isVisible={isVisible}
animationIn="slideInUp"
animationOut="slideOutDown"
animationInTiming={600}
animationOutTiming={600}
onBackdropPress={() => setVisible(false)}>
<View style={{ backgroundColor: "white", padding: 20 }}>
<Text>This is a modal with custom animations!</Text>
<Button title="Close" onPress={() => setVisible(false)} />
</View>
</Modal>
</View>
);
}import React, { useState } from "react";
import { Text, View } from "react-native";
import Modal from "react-native-modal";
function SwipeableModal() {
const [isVisible, setVisible] = useState(true);
return (
<Modal
isVisible={isVisible}
swipeDirection={["up", "down"]}
swipeThreshold={100}
onSwipeComplete={() => setVisible(false)}
onSwipeStart={(gestureState) => console.log("Swipe started")}
onSwipeMove={(percentageShown) => console.log("Swipe progress:", percentageShown)}>
<View style={{ backgroundColor: "white", padding: 20 }}>
<Text>Swipe up or down to dismiss this modal</Text>
</View>
</Modal>
);
}import React, { useState } from "react";
import { Text, View, ImageBackground } from "react-native";
import Modal from "react-native-modal";
function CustomBackdropModal() {
const [isVisible, setVisible] = useState(true);
const CustomBackdrop = () => (
<ImageBackground
source={{ uri: "https://example.com/background.jpg" }}
style={{ flex: 1 }}
blurRadius={5}
/>
);
return (
<Modal
isVisible={isVisible}
customBackdrop={<CustomBackdrop />}
onBackdropPress={() => setVisible(false)}>
<View style={{ backgroundColor: "white", padding: 20 }}>
<Text>Modal with custom backdrop</Text>
</View>
</Modal>
);
}import React, { useState } from "react";
import { TextInput, Button, View } from "react-native";
import Modal from "react-native-modal";
function KeyboardModal() {
const [isVisible, setVisible] = useState(true);
const [text, setText] = useState("");
return (
<Modal
isVisible={isVisible}
avoidKeyboard={true}
onBackdropPress={() => setVisible(false)}>
<View style={{ backgroundColor: "white", padding: 20 }}>
<TextInput
value={text}
onChangeText={setText}
placeholder="Type something..."
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<Button title="Close" onPress={() => setVisible(false)} />
</View>
</Modal>
);
}import React, { useState } from "react";
import { Text, Button, View } from "react-native";
import Modal from "react-native-modal";
function FullScreenModal() {
const [isVisible, setVisible] = useState(true);
return (
<Modal
isVisible={isVisible}
style={{ margin: 0 }} // Remove default margin for full screen
animationIn="slideInRight"
animationOut="slideOutRight">
<View style={{ flex: 1, backgroundColor: "white", padding: 20 }}>
<Text>Full screen modal</Text>
<Button title="Close" onPress={() => setVisible(false)} />
</View>
</Modal>
);
}