logoChisa UI

Command Palette

Search for a command to run...

Componentschevron_rightBottomDrawer

BottomDrawer

Bottom sheet with smooth slide animations and gesture-based drag to close.

9:41
signal_cellular_altwifibattery_full

Tap to open drawer

Preview Controls

Animation

Slides up from bottom with drag to close

This is a CSS-based preview of the animation. The actual React Native component uses physics-based springs for a more natural feel.

Installation

$

Usage

Example.tsx
import { useState } from 'react';
import { View, Text, Button } from 'react-native';
import { BottomDrawer } from './components/BottomDrawer';
export default function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<View style={{ flex: 1 }}>
<Button title="Open Drawer" onPress={() => setIsOpen(true)} />
<BottomDrawer isOpen={isOpen} onClose={() => setIsOpen(false)} height={400}>
<Text style={{ fontSize: 20, fontWeight: 'bold' }}>Drawer Content</Text>
<Text style={{ marginTop: 8, color: '#666' }}>
Drag the handle down to close, or tap the backdrop.
</Text>
</BottomDrawer>
</View>
);
}

Full Component Code

BottomDrawer.tsx
1import React, { useEffect, useCallback } from 'react';
2import { View, StyleSheet, Dimensions, TouchableWithoutFeedback, ViewStyle, StyleProp } from 'react-native';
3import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, Easing } from 'react-native-reanimated';
4import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
5
6const { height: SCREEN_HEIGHT } = Dimensions.get('window');
7
8interface BottomDrawerProps {
9 isOpen: boolean;
10 onClose: () => void;
11 children: React.ReactNode;
12 height?: number;
13 duration?: number;
14 handleHeight?: number;
15 backdropOpacity?: number;
16 style?: StyleProp<ViewStyle>;
17}
18
19export const BottomDrawer: React.FC<BottomDrawerProps> = ({
20 isOpen, onClose, children,
21 height = SCREEN_HEIGHT * 0.5,
22 duration = 300,
23 handleHeight = 24,
24 backdropOpacity = 0.5,
25 style,
26}) => {
27 const translateY = useSharedValue(height);
28 const context = useSharedValue({ y: 0 });
29 const backdropVisible = useSharedValue(0);
30
31 const timingConfig = { duration, easing: Easing.out(Easing.ease) };
32
33 const closeDrawer = useCallback(() => {
34 'worklet';
35 translateY.value = withTiming(height, timingConfig, () => runOnJS(onClose)());
36 backdropVisible.value = withTiming(0, { duration: duration / 2 });
37 }, [height, duration, onClose]);
38
39 useEffect(() => {
40 if (isOpen) {
41 translateY.value = withTiming(0, timingConfig);
42 backdropVisible.value = withTiming(1, { duration });
43 } else {
44 translateY.value = withTiming(height, timingConfig);
45 backdropVisible.value = withTiming(0, { duration: duration / 2 });
46 }
47 }, [isOpen]);
48
49 const gesture = Gesture.Pan()
50 .onStart(() => { context.value = { y: translateY.value }; })
51 .onUpdate((e) => {
52 translateY.value = Math.max(0, context.value.y + e.translationY);
53 backdropVisible.value = Math.max(0, Math.min(1, 1 - translateY.value / height));
54 })
55 .onEnd((e) => {
56 if (translateY.value > height * 0.25 || e.velocityY > 500) {
57 closeDrawer();
58 } else {
59 translateY.value = withTiming(0, timingConfig);
60 backdropVisible.value = withTiming(1, { duration: duration / 2 });
61 }
62 });
63
64 const animatedDrawerStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }] }));
65 const animatedBackdropStyle = useAnimatedStyle(() => ({
66 opacity: backdropVisible.value * backdropOpacity,
67 pointerEvents: backdropVisible.value > 0 ? 'auto' : 'none',
68 }));
69
70 if (!isOpen && translateY.value >= height) return null;
71
72 return (
73 <GestureHandlerRootView style={StyleSheet.absoluteFill}>
74 <TouchableWithoutFeedback onPress={onClose}>
75 <Animated.View style={[styles.backdrop, animatedBackdropStyle]} />
76 </TouchableWithoutFeedback>
77 <Animated.View style={[styles.drawer, { height: height + handleHeight }, style, animatedDrawerStyle]}>
78 <GestureDetector gesture={gesture}>
79 <View style={[styles.handleContainer, { height: handleHeight }]}>
80 <View style={styles.handle} />
81 </View>
82 </GestureDetector>
83 <View style={styles.content}>{children}</View>
84 </Animated.View>
85 </GestureHandlerRootView>
86 );
87};
88
89const styles = StyleSheet.create({
90 backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000' },
91 drawer: { position: 'absolute', left: 0, right: 0, bottom: 0, backgroundColor: '#fff',
92 borderTopLeftRadius: 24, borderTopRightRadius: 24, elevation: 20 },
93 handleContainer: { alignItems: 'center', justifyContent: 'center' },
94 handle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#E0E0E0' },
95 content: { flex: 1, paddingHorizontal: 16 },
96});
97
98export default BottomDrawer;

Props

NameTypeDefaultDescription
isOpenbooleanfalseWhether the drawer is open
onClose() => void-Callback when drawer closes
childrenReact.ReactNode-Drawer content
heightnumber50% of screenHeight of the drawer
durationnumber300Animation duration in ms
handleHeightnumber24Height of the drag handle area
backdropOpacitynumber0.5Opacity of backdrop when open