Drawer

A sliding panel that enters from the edge of the screen, commonly used for navigation or filters in mobile applications.

Installation

Install the component:

npx natively-ui add drawer

Usage

DrawerDemo.jsx
1import { Drawer } from '@/components/ui/drawer'; 2 3export default function MyComponent() { 4 const [isOpen, setIsOpen] = React.useState(false); 5 6 return ( 7 <> 8 <Button onPress={() => setIsOpen(true)}> 9 Open Drawer 10 </Button> 11 12 <Drawer 13 isOpen={isOpen} 14 onClose={() => setIsOpen(false)} 15 side="left" 16 > 17 <View style={{ padding: 16 }}> 18 <Text style={{ fontSize: 20, fontWeight: 'bold' }}> 19 Navigation 20 </Text> 21 <View style={{ marginTop: 16 }}> 22 <Text>Menu Item 1</Text> 23 <Text>Menu Item 2</Text> 24 <Text>Menu Item 3</Text> 25 </View> 26 </View> 27 </Drawer> 28 </> 29 ); 30}

Props

PropTypeDefaultDescription
isOpenbooleanfalseControls the visibility of the drawer.
onClose() => void-Callback when the drawer is closed.
side'left' | 'right' | 'top' | 'bottom''left'The side from which the drawer appears.
widthnumber300Width of the drawer (for left/right drawers).
heightnumber300Height of the drawer (for top/bottom drawers).
overlayColorstring'rgba(0, 0, 0, 0.5)'Color of the overlay behind the drawer.
overlayClosebooleantrueWhether clicking the overlay closes the drawer.
closeOnEscapebooleantrueWhether pressing escape closes the drawer.
childrenReact.ReactNode-The content to display inside the drawer.
classNamestring""Additional CSS classes for the drawer container.
enableGesturesbooleanfalseEnable swipe gestures to close the drawer.

Features

  • Smooth slide animations with React Native Reanimated
  • Multiple placement options (left, right, top, bottom)
  • Customizable dimensions and overlay styling
  • Backdrop overlay with tap-to-close functionality
  • Gesture-based interactions for intuitive closing
  • Keyboard navigation support with escape key
  • Accessible focus management and screen reader support
  • Safe area insets handling for notched devices
  • Portal rendering for proper z-index stacking
  • Customizable animation duration and easing

Examples

Navigation Drawer

NavigationDrawer.jsx
1function NavigationDrawer() { 2 const [isOpen, setIsOpen] = useState(false); 3 4 const navigationItems = [ 5 { title: 'Home', icon: 'home' }, 6 { title: 'Profile', icon: 'user' }, 7 { title: 'Settings', icon: 'settings' }, 8 { title: 'Help', icon: 'help-circle' }, 9 ]; 10 11 return ( 12 <> 13 <Button 14 leftIcon={<Icon name="menu" size={16} />} 15 onPress={() => setIsOpen(true)} 16 > 17 Menu 18 </Button> 19 20 <Drawer 21 isOpen={isOpen} 22 onClose={() => setIsOpen(false)} 23 side="left" 24 width={280} 25 > 26 <View style={{ padding: 20 }}> 27 <Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 20 }}> 28 Navigation 29 </Text> 30 31 {navigationItems.map((item, index) => ( 32 <Pressable 33 key={index} 34 style={{ 35 flexDirection: 'row', 36 alignItems: 'center', 37 paddingVertical: 12, 38 paddingHorizontal: 16, 39 borderRadius: 8, 40 }} 41 onPress={() => { 42 console.log(`Navigate to ${item.title}`); 43 setIsOpen(false); 44 }} 45 > 46 <Icon name={item.icon} size={20} style={{ marginRight: 12 }} /> 47 <Text style={{ fontSize: 16 }}>{item.title}</Text> 48 </Pressable> 49 ))} 50 </View> 51 </Drawer> 52 </> 53 ); 54}

Bottom Sheet

BottomSheet.jsx
1function BottomSheet() { 2 const [isOpen, setIsOpen] = useState(false); 3 4 return ( 5 <> 6 <Button onPress={() => setIsOpen(true)}> 7 Show Options 8 </Button> 9 10 <Drawer 11 isOpen={isOpen} 12 onClose={() => setIsOpen(false)} 13 side="bottom" 14 height={400} 15 overlayColor="rgba(0, 0, 0, 0.7)" 16 enableGestures={true} 17 > 18 <View style={{ padding: 20 }}> 19 <View style={{ 20 width: 40, 21 height: 4, 22 backgroundColor: '#ccc', 23 borderRadius: 2, 24 alignSelf: 'center', 25 marginBottom: 20 26 }} /> 27 28 <Text style={{ fontSize: 20, fontWeight: 'bold', marginBottom: 16 }}> 29 Quick Actions 30 </Text> 31 32 <View style={{ gap: 12 }}> 33 <Button variant="outline">Share</Button> 34 <Button variant="outline">Edit</Button> 35 <Button variant="outline">Duplicate</Button> 36 <Button variant="destructive">Delete</Button> 37 </View> 38 </View> 39 </Drawer> 40 </> 41 ); 42}

Filter Drawer

FilterDrawer.jsx
1function FilterDrawer() { 2 const [isOpen, setIsOpen] = useState(false); 3 const [filters, setFilters] = useState({ 4 category: 'all', 5 priceRange: [0, 1000], 6 inStock: false 7 }); 8 9 return ( 10 <> 11 <Button 12 rightIcon={<Icon name="filter" size={16} />} 13 onPress={() => setIsOpen(true)} 14 > 15 Filters 16 </Button> 17 18 <Drawer 19 isOpen={isOpen} 20 onClose={() => setIsOpen(false)} 21 side="right" 22 width={320} 23 overlayClose={false} 24 > 25 <View style={{ padding: 20, flex: 1 }}> 26 <View style={{ 27 flexDirection: 'row', 28 justifyContent: 'space-between', 29 alignItems: 'center', 30 marginBottom: 24 31 }}> 32 <Text style={{ fontSize: 20, fontWeight: 'bold' }}> 33 Filters 34 </Text> 35 <Button 36 size="icon" 37 variant="ghost" 38 onPress={() => setIsOpen(false)} 39 > 40 <Icon name="x" size={20} /> 41 </Button> 42 </View> 43 44 <View style={{ gap: 24, flex: 1 }}> 45 <View> 46 <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}> 47 Category 48 </Text> 49 {/* Category selection components */} 50 </View> 51 52 <View> 53 <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}> 54 Price Range 55 </Text> 56 {/* Price range slider component */} 57 </View> 58 59 <View> 60 <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}> 61 Availability 62 </Text> 63 {/* Checkbox component */} 64 </View> 65 </View> 66 67 <View style={{ flexDirection: 'row', gap: 12, marginTop: 'auto' }}> 68 <Button 69 variant="outline" 70 style={{ flex: 1 }} 71 onPress={() => setFilters({ category: 'all', priceRange: [0, 1000], inStock: false })} 72 > 73 Reset 74 </Button> 75 <Button 76 style={{ flex: 1 }} 77 onPress={() => { 78 console.log('Apply filters:', filters); 79 setIsOpen(false); 80 }} 81 > 82 Apply 83 </Button> 84 </View> 85 </View> 86 </Drawer> 87 </> 88 ); 89}

Multiple Directions

MultiDirectionDrawers.jsx
1function MultiDirectionDrawers() { 2 const [activeDrawer, setActiveDrawer] = useState(null); 3 4 const drawers = [ 5 { side: 'left', label: 'Left Menu', width: 280 }, 6 { side: 'right', label: 'Right Panel', width: 320 }, 7 { side: 'top', label: 'Top Banner', height: 200 }, 8 { side: 'bottom', label: 'Bottom Sheet', height: 300 } 9 ]; 10 11 return ( 12 <View style={{ gap: 12, padding: 20 }}> 13 {drawers.map((drawer) => ( 14 <Button 15 key={drawer.side} 16 variant="outline" 17 onPress={() => setActiveDrawer(drawer.side)} 18 > 19 Open {drawer.label} 20 </Button> 21 ))} 22 23 {drawers.map((drawer) => ( 24 <Drawer 25 key={drawer.side} 26 isOpen={activeDrawer === drawer.side} 27 onClose={() => setActiveDrawer(null)} 28 side={drawer.side} 29 width={drawer.width} 30 height={drawer.height} 31 > 32 <View style={{ 33 padding: 20, 34 alignItems: 'center', 35 justifyContent: 'center', 36 flex: 1 37 }}> 38 <Text style={{ fontSize: 18, fontWeight: 'bold' }}> 39 {drawer.label} 40 </Text> 41 <Text style={{ marginTop: 8, textAlign: 'center' }}> 42 This drawer slides in from the {drawer.side} 43 </Text> 44 <Button 45 style={{ marginTop: 16 }} 46 onPress={() => setActiveDrawer(null)} 47 > 48 Close 49 </Button> 50 </View> 51 </Drawer> 52 ))} 53 </View> 54 ); 55}

Nested Drawers

NestedDrawers.jsx
1function NestedDrawers() { 2 const [primaryOpen, setPrimaryOpen] = useState(false); 3 const [secondaryOpen, setSecondaryOpen] = useState(false); 4 5 return ( 6 <> 7 <Button onPress={() => setPrimaryOpen(true)}> 8 Open Main Menu 9 </Button> 10 11 <Drawer 12 isOpen={primaryOpen} 13 onClose={() => setPrimaryOpen(false)} 14 side="left" 15 width={280} 16 > 17 <View style={{ padding: 20 }}> 18 <Text style={{ fontSize: 20, fontWeight: 'bold', marginBottom: 20 }}> 19 Main Menu 20 </Text> 21 22 <Button 23 variant="outline" 24 style={{ marginBottom: 12 }} 25 onPress={() => setSecondaryOpen(true)} 26 > 27 Open Sub Menu 28 </Button> 29 30 <Button variant="outline"> 31 Other Option 32 </Button> 33 </View> 34 </Drawer> 35 36 <Drawer 37 isOpen={secondaryOpen} 38 onClose={() => setSecondaryOpen(false)} 39 side="right" 40 width={250} 41 overlayColor="rgba(0, 0, 0, 0.3)" 42 > 43 <View style={{ padding: 20 }}> 44 <Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 16 }}> 45 Sub Menu 46 </Text> 47 48 <Text>Nested drawer content</Text> 49 50 <Button 51 style={{ marginTop: 20 }} 52 onPress={() => setSecondaryOpen(false)} 53 > 54 Close Sub Menu 55 </Button> 56 </View> 57 </Drawer> 58 </> 59 ); 60}