npx create-expo-app BottomSheetAnimation -t expo-template-blank-typescriptcd BottomSheetAnimation
npx expo install react-native-reanimated react-native-gesture-handler
import { StatusBar } from 'expo-status-bar';import { StyleSheet, View } from 'react-native';import { gestureHandlerRootHOC } from 'react-native-gesture-handler';function App() {return (<View style={styles.container}><StatusBar style="light" /></View>);}const styles = StyleSheet.create({container: {flex: 1,backgroundColor: '#111',},});export default gestureHandlerRootHOC(App);
import { Dimensions, StyleSheet, View } from 'react-native';import React from 'react';import { GestureDetector } from 'react-native-gesture-handler';import Animated from 'react-native-reanimated';const { height: SCREEN_HEIGHT } = Dimensions.get('window');type BottomSheetProps = {children?: React.ReactNode;};const BottomSheet: React.FC<BottomSheetProps> = ({ children }) => {const gesture = Gesture.Pan();return (<GestureDetector gesture={gesture}><Animated.View style={[styles.bottomSheetContainer]}><View style={styles.line} />{children}</Animated.View></GestureDetector>);};const styles = StyleSheet.create({bottomSheetContainer: {height: SCREEN_HEIGHT,width: '100%',backgroundColor: 'white',position: 'absolute',top: SCREEN_HEIGHT,borderRadius: 25,},line: {width: 75,height: 4,backgroundColor: 'grey',alignSelf: 'center',marginVertical: 15,borderRadius: 2,},});export default BottomSheet;
import { Gesture, GestureDetector } from 'react-native-gesture-handler';import Animated, {useAnimatedStyle,useSharedValue,withSpring,} from 'react-native-reanimated';const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50;const BottomSheet: React.FC<BottomSheetProps> = ({ children }) => {const translateY = useSharedValue(0);const context = useSharedValue({ y: 0 });const gesture = Gesture.Pan().onStart(() => {context.value = { y: translateY.value };}).onUpdate((event) => {translateY.value = event.translationY + context.value.y;translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);}).onEnd(() => {if (translateY.value > -SCREEN_HEIGHT / 3) {translateY.value = withSpring(0);} else if (translateY.value < -SCREEN_HEIGHT / 1.5) {translateY.value = withSpring(MAX_TRANSLATE_Y);}});const rBottomSheetStyle = useAnimatedStyle(() => {return {transform: [{ translateY: translateY.value }],};});return (<GestureDetector gesture={gesture}><Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}><View style={styles.line} />{children}</Animated.View></GestureDetector>);};
import {Extrapolate,interpolate,useAnimatedStyle,useSharedValue,withSpring,} from 'react-native-reanimated';// ... inside the BottomSheet componentconst rBottomSheetStyle = useAnimatedStyle(() => {const borderRadius = interpolate(translateY.value,[MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],[25, 5],Extrapolate.CLAMP);return {borderRadius,transform: [{ translateY: translateY.value }],};});
import React, { useCallback, useImperativeHandle, forwardRef } from 'react';export type BottomSheetRefProps = {scrollTo: (destination: number) => void;isActive: () => boolean;};const BottomSheet = forwardRef<BottomSheetRefProps, BottomSheetProps>(({ children }, ref) => {const translateY = useSharedValue(0);const active = useSharedValue(false);const scrollTo = useCallback((destination: number) => {'worklet';active.value = destination !== 0;translateY.value = withSpring(destination, { damping: 50 });}, []);const isActive = useCallback(() => {return active.value;}, []);useImperativeHandle(ref, () => ({ scrollTo, isActive }), [scrollTo,isActive,]);// ... rest of the component implementation});
type AnimatedBackdropProps = {active: SharedValue<boolean>;onTap: () => void;};const AnimatedBackdrop: React.FC<AnimatedBackdropProps> = ({active,onTap,}) => {const rBackdropStyle = useAnimatedStyle(() => {return {// if the sheet is active, the backdrop is visibleopacity: withTiming(active.value ? 1 : 0, { duration: 200 }),// if the sheet isn't active, the backdrop shouldn't be touchable (otherwise the background is blocked)pointerEvents: active.value ? 'auto' : 'none',};});return (<Animated.Viewstyle={[StyleSheet.absoluteFill, styles.container, rBackdropStyle]}/>);};const styles = StyleSheet.create({container: {// the backdrop is a semi-transparent white color (but this depends on your design)backgroundColor: 'rgba(255, 255, 255, 0.05)',},});
const BottomSheet: React.FC<BottomSheetProps> = ({ children }) => {// ... existing codereturn (<><AnimatedBackdropactive={active}onTap={() => {// when the backdrop is tapped, we close the bottom sheetscrollTo(0);}}/><GestureDetector gesture={gesture}><Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}><View style={styles.line} />{children}</Animated.View></GestureDetector></>);};
Every week I send out a newsletter sharing new things about React Native animations.