I've created a reusable BottomSheet
component that accepts any content.
This is how it looks like:

Here's the code (in TypeScript) of the BottomSheet
component:
import * as React from 'react'
import {
Animated,
Easing,
Pressable,
StyleSheet,
useWindowDimensions,
View,
} from 'react-native'
const DEFAULT_HEIGHT = 300
function useAnimatedBottom(show: boolean, height: number = DEFAULT_HEIGHT) {
const animatedValue = React.useRef(new Animated.Value(0))
const bottom = animatedValue.current.interpolate({
inputRange: [0, 1],
outputRange: [-height, 0],
})
React.useEffect(() => {
if (show) {
Animated.timing(animatedValue.current, {
toValue: 1,
duration: 350,
// Accelerate then decelerate - https://cubic-bezier.com/#.28,0,.63,1
easing: Easing.bezier(0.28, 0, 0.63, 1),
useNativeDriver: false, // 'bottom' is not supported by native animated module
}).start()
} else {
Animated.timing(animatedValue.current, {
toValue: 0,
duration: 250,
// Accelerate - https://easings.net/#easeInCubic
easing: Easing.cubic,
useNativeDriver: false,
}).start()
}
}, [show])
return bottom
}
interface Props {
children: React.ReactNode
show: boolean
height?: number
onOuterClick?: () => void
}
export function BottomSheet({
children,
show,
height = DEFAULT_HEIGHT,
onOuterClick,
}: Props) {
const { height: screenHeight } = useWindowDimensions()
const bottom = useAnimatedBottom(show, height)
return (
<>
{/* Outer semitransparent overlay - remove it if you don't want it */}
{show && (
<Pressable
onPress={onOuterClick}
style={[styles.outerOverlay, { height: screenHeight }]}
>
<View />
</Pressable>
)}
<Animated.View style={[styles.bottomSheet, { height, bottom }]}>
{children}
</Animated.View>
</>
)
}
const styles = StyleSheet.create({
outerOverlay: {
position: 'absolute',
width: '100%',
zIndex: 1,
backgroundColor: 'black',
opacity: 0.3,
},
bottomSheet: {
position: 'absolute',
width: '100%',
zIndex: 1,
// Here you can set a common style for all bottom sheets, or nothing if you
// want different designs
backgroundColor: 'dodgerblue',
borderRadius: 16,
},
})
I put this code in a file named BottomSheet.tsx
.
This is how you use the BottomSheet
:
import * as React from 'react'
import {
Pressable,
SafeAreaView,
StatusBar,
StyleSheet,
Text,
View,
} from 'react-native'
import { BottomSheet } from './src/BottomSheet'
const App = () => {
const [showBottomSheet, setShowBottomSheet] = React.useState(false)
const hide = () => {
setShowBottomSheet(false)
}
return (
<SafeAreaView style={styles.safeAreaView}>
<StatusBar barStyle={'dark-content'} />
<View style={styles.container}>
<Pressable
onPress={() => {
setShowBottomSheet(true)
}}
style={styles.showButton}
>
<Text style={styles.buttonText}>Show bottom sheet</Text>
</Pressable>
</View>
<BottomSheet show={showBottomSheet} height={290} onOuterClick={hide}>
<View style={styles.bottomSheetContent}>
<Text style={styles.bottomSheetText}>Hey boys, hey girls!</Text>
<Pressable onPress={hide} style={styles.bottomSheetCloseButton}>
<Text style={styles.buttonText}>X Close</Text>
</Pressable>
</View>
</BottomSheet>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safeAreaView: {
flex: 1,
},
container: {
flex: 1,
},
showButton: {
marginTop: 48,
padding: 16,
backgroundColor: 'mediumspringgreen',
alignSelf: 'center',
borderRadius: 8,
},
buttonText: {
fontSize: 20,
},
bottomSheetContent: {
padding: 40,
alignItems: 'center',
},
bottomSheetText: {
fontSize: 24,
marginBottom: 80,
},
bottomSheetCloseButton: {
padding: 16,
backgroundColor: 'deeppink',
borderRadius: 8,
},
})
export default App
Notes:
- I've put an outer semitransparent overlay, but you can get rid of it by deleting it.
- I've chosen to close the modal when you click on the outer semitransparent overlay. This is done with the
onOuterClick
callback, which is optional - don't pass it if you don't want to do anything.
- I've put some styling (blue background + border radius) that applies to all bottom sheets, but you can remove it if you want to have different styles.