A smooth animated modal component for React Native that is keyboard-aware and can be used as a controller. Built with react-native-reanimated and react-native-gesture-handler for performant animations and gestures.
- 🎹 Keyboard Aware - Automatically adjusts when keyboard appears/disappears
- 🎨 Smooth Animations - Customizable spring and timing animations
- 👆 Draggable - Drag to dismiss with snap points support
- 🎛️ Modal Controller - Context-based API for easy modal management
- 🎯 Flexible Usage - Use with ModalController or directly as a component
- 🎨 Highly Customizable - Custom headers, styles, animations, and more
- 📱 TypeScript Support - Full TypeScript definitions included
- ⚡ Performant - Built with Reanimated for 60fps animations
npm install react-native-keyboard-aware-modalor
yarn add react-native-keyboard-aware-modalThis library requires the following peer dependencies:
npm install react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-workletsImportant: Make sure to follow the setup instructions for:
The ModalController provides a context-based API for managing modals throughout your app.
import { ModalControllerProvider } from 'react-native-keyboard-aware-modal';
function App() {
return (
<ModalControllerProvider>
{/* Your app content */}
</ModalControllerProvider>
);
}import { useModal } from 'react-native-keyboard-aware-modal';
import { View, Text, TouchableOpacity } from 'react-native';
function MyComponent() {
const { showModal } = useModal();
const openModal = () => {
showModal(
() => (
<View style={{ padding: 20 }}>
<Text>Hello from modal!</Text>
</View>
),
{
// Optional configuration
backgroundColor: 'white',
borderRadius: 20,
keyboardAware: true,
}
);
};
return (
<TouchableOpacity onPress={openModal}>
<Text>Open Modal</Text>
</TouchableOpacity>
);
}You can also use KeyboardAwareModal directly without the controller context:
import { KeyboardAwareModal } from 'react-native-keyboard-aware-modal';
import { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
function MyComponent() {
const [visible, setVisible] = useState(false);
return (
<>
<TouchableOpacity onPress={() => setVisible(true)}>
<Text>Open Modal</Text>
</TouchableOpacity>
<KeyboardAwareModal
visible={visible}
onClose={() => setVisible(false)}
keyboardAware={true}
backgroundColor="white"
borderRadius={20}>
<View style={{ padding: 20 }}>
<Text>Hello from modal!</Text>
</View>
</KeyboardAwareModal>
</>
);
}Check out the examples folder for comprehensive examples:
- BasicModalExample - Basic modal usage with different configurations
- CustomHeaderExample - Creating custom headers
- DraggableModalExample - Draggable modals with snap points
- AnimationCustomizationExample - Customizing animations
- KeyboardAwareExample - Keyboard-aware forms
- DirectModalExample - Direct component usage
To run the examples:
cd example
yarn install
yarn ios # or yarn androidProvider component that wraps your app to enable modal controller functionality.
<ModalControllerProvider>
{children}
</ModalControllerProvider>Returns an object with showModal and hideModal functions.
const { showModal, hideModal } = useModal();renderContent: A function that returns the modal content (ReactNode)config?: Optional ModalConfig object
Closes the currently visible modal.
| Prop | Type | Description | Default |
|---|---|---|---|
visible |
boolean |
Controls modal visibility | required |
onClose |
() => void |
Callback when modal should close | required |
children |
ReactNode |
Modal content | required |
headerView |
ReactNode |
Custom header component | undefined |
snapPoints |
number[] |
Array of heights (0-1) where modal can snap | [0] |
initialSnapIndex |
number |
Initial snap point index | 0 |
dragHandle |
boolean |
Show drag handle indicator | false |
dragHandleStyle |
ViewStyle |
Custom drag handle style | undefined |
extraClosingThreshold |
number |
Extra pixels to drag before closing | 40 |
draggable |
boolean |
Enable drag to dismiss | true |
keyboardAware |
boolean |
Automatically adjust for keyboard | true |
backgroundColor |
string |
Modal background color | 'white' |
backdropColor |
string |
Backdrop color | 'rgba(0, 0, 0, 0.5)' |
backdropOpacity |
number |
Backdrop opacity (0-1) | 1 |
showBackdrop |
boolean |
Show backdrop overlay | true |
roundedCorners |
boolean |
Round top corners | true |
borderRadius |
number |
Border radius for top corners | 20 |
openingAnimation |
ModalAnimationConfig |
Custom opening animation | undefined |
closingAnimation |
ModalAnimationConfig |
Custom closing animation | undefined |
backdropAnimation |
BackdropAnimationConfig |
Custom backdrop animation | undefined |
mainContainerStyle |
ViewStyle |
Main container style | undefined |
modalContentStyle |
ViewStyle |
Modal content wrapper style | undefined |
headerStyle |
ViewStyle |
Header container style | undefined |
contentStyle |
ViewStyle |
Content container style | undefined |
contentContainerStyle |
ViewStyle |
ScrollView content container style | undefined |
maxModalHeight |
number |
Maximum height for the modal content | undefined |
layoutAnimation |
LayoutAnimation |
Custom layout animation for the modal container | LinearTransition.springify().damping(25).stiffness(120).duration(500) |
Configuration object for showModal:
interface ModalConfig {
headerView?: ReactNode;
snapPoints?: number[];
initialSnapIndex?: number;
dragHandle?: boolean;
dragHandleStyle?: ViewStyle;
extraClosingThreshold?: number;
draggable?: boolean;
keyboardAware?: boolean;
backgroundColor?: string;
backdropColor?: string;
backdropOpacity?: number;
showBackdrop?: boolean;
roundedCorners?: boolean;
borderRadius?: number;
openingAnimation?: ModalAnimationConfig;
closingAnimation?: ModalAnimationConfig;
backdropAnimation?: BackdropAnimationConfig;
mainContainerStyle?: ViewStyle;
modalContentStyle?: ViewStyle;
headerStyle?: ViewStyle;
contentStyle?: ViewStyle;
contentContainerStyle?: ViewStyle;
maxModalHeight?: number;
layoutAnimation?: LayoutAnimation;
}interface ModalAnimationConfig {
type?: 'spring' | 'timing';
springConfig?: {
damping?: number;
stiffness?: number;
mass?: number;
overshootClamping?: boolean;
restDisplacementThreshold?: number;
restSpeedThreshold?: number;
velocity?: number;
};
timingConfig?: {
duration?: number;
easing?: EasingFunction;
};
}interface BackdropAnimationConfig {
duration?: number;
easing?: EasingFunction;
}showModal(
() => (
<View style={{ padding: 20 }}>
<Text>Drag me up and down!</Text>
</View>
),
{
snapPoints: [0.5, 0.75, 1], // 50%, 75%, 100% of screen height
initialSnapIndex: 1, // Start at 100%
dragHandle: true,
}
);showModal(
() => <View>...</View>,
{
openingAnimation: {
type: 'spring',
springConfig: {
damping: 20,
stiffness: 90,
},
},
closingAnimation: {
type: 'timing',
timingConfig: {
duration: 200,
},
},
}
);showModal(
() => <View>...</View>,
{
headerView: (
<View style={{ padding: 16, backgroundColor: '#f0f0f0' }}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>
Custom Header
</Text>
</View>
),
}
);MIT
Made with create-react-native-library