Drawer
A bottom sheet component that slides up from the bottom of the screen with gesture-based dismissal.
The Drawer component is a bottom sheet overlay that slides up from the bottom of the screen. It supports swipe-to-dismiss gestures, animated transitions with React Native Reanimated, a drag handle indicator, and a compound component API for full flexibility.
Preview
Installation
npx novaui-cli add drawerDependencies
The Drawer requires the following peer dependencies:
react-native-reanimated>= 3react-native-gesture-handler>= 2lucide-react-native>= 0.300
Import
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
DrawerClose,
} from 'novaui-components'Basic Usage
The Drawer is a compound component with these parts:
Drawer- Root container with open/close stateDrawerTrigger- Element that opens the drawerDrawerContent- The sliding panel with overlay, drag handle, and content areaDrawerHeader- Groups the title and descriptionDrawerFooter- Bottom section for action buttonsDrawerTitle- The drawer headingDrawerDescription- Subtitle textDrawerClose- Element that closes the drawer
import {
Drawer, DrawerTrigger, DrawerContent,
DrawerHeader, DrawerFooter, DrawerTitle,
DrawerDescription, DrawerClose,
} from 'novaui-components'
import { Button } from 'novaui-components'
export function BasicDrawer() {
return (
<Drawer>
<DrawerTrigger>
<Button variant="outline" label="Open Drawer" />
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move Goal</DrawerTitle>
<DrawerDescription>Set your daily activity goal.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button label="Submit" />
<DrawerClose asChild>
<Button variant="outline" label="Cancel" />
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}With Content
Add any content inside the drawer body:
import {
Drawer, DrawerTrigger, DrawerContent,
DrawerHeader, DrawerFooter, DrawerTitle,
DrawerDescription, DrawerClose,
} from 'novaui-components'
import { Button } from 'novaui-components'
import { View, Text, TextInput } from 'react-native'
export function ProfileDrawer() {
return (
<Drawer>
<DrawerTrigger>
<Button variant="outline" label="Edit Profile" />
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Edit Profile</DrawerTitle>
<DrawerDescription>Make changes to your profile here.</DrawerDescription>
</DrawerHeader>
<View className="p-4 gap-3">
<View className="gap-1.5">
<Text className="text-sm font-medium">Name</Text>
<TextInput
defaultValue="John Doe"
className="h-10 rounded-md border border-input bg-background px-3"
/>
</View>
<View className="gap-1.5">
<Text className="text-sm font-medium">Username</Text>
<TextInput
defaultValue="@johndoe"
className="h-10 rounded-md border border-input bg-background px-3"
/>
</View>
</View>
<DrawerFooter>
<Button label="Save changes" />
<DrawerClose asChild>
<Button variant="outline" label="Cancel" />
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Controlled State
Control the drawer's open/close state programmatically:
import { useState } from 'react'
export function ControlledDrawer() {
const [open, setOpen] = useState(false)
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger>
<Button variant="outline" label="Controlled Drawer" />
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Controlled</DrawerTitle>
<DrawerDescription>
This drawer's state is managed externally via the open and onOpenChange props.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button label="Got it" onPress={() => setOpen(false)} />
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Action Sheet Style
Use the drawer as an action sheet with a list of options:
import {
Drawer, DrawerTrigger, DrawerContent,
DrawerHeader, DrawerTitle, DrawerDescription,
} from 'novaui-components'
import { Button } from 'novaui-components'
import { View, Text, Pressable } from 'react-native'
const options = ['Copy Link', 'Share to Twitter', 'Share to Facebook', 'Send via Email']
export function ShareDrawer() {
return (
<Drawer>
<DrawerTrigger>
<Button variant="outline" label="Share" />
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Share</DrawerTitle>
<DrawerDescription>Choose how you want to share this.</DrawerDescription>
</DrawerHeader>
<View className="p-4 gap-1">
{options.map((option) => (
<Pressable
key={option}
className="flex-row items-center gap-3 rounded-md px-3 py-2.5 active:bg-accent"
>
<Text className="text-sm">{option}</Text>
</Pressable>
))}
</View>
</DrawerContent>
</Drawer>
)
}Props API
Drawer Props
Extends React.ComponentPropsWithoutRef<typeof View> and includes:
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Callback when open state changes |
shouldScaleBackground | boolean | true | Whether to scale the background when open |
DrawerTrigger Props
Extends React.ComponentPropsWithoutRef<typeof Pressable>:
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as the child element instead of a Pressable |
DrawerContent Props
Extends React.ComponentPropsWithoutRef<typeof View>:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the drawer panel |
DrawerHeader Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DrawerFooter Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DrawerTitle Props
Extends React.ComponentPropsWithoutRef<typeof Text>:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DrawerDescription Props
Extends React.ComponentPropsWithoutRef<typeof Text>:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
DrawerClose Props
Extends React.ComponentPropsWithoutRef<typeof Pressable>:
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as the child element instead of a Pressable |
Gesture & Animation
The Drawer uses react-native-reanimated and react-native-gesture-handler:
- Slide animation: Content slides in from the bottom with
SlideInDown/SlideOutDown - Overlay fade: Background fades in/out with
FadeIn/FadeOut - Swipe to dismiss: Pan gesture detects swipe down — dismisses when dragged > 100px or velocity > 500
- Spring back: If swipe is too small, content springs back to position
- Drag handle: Visual indicator (
mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted) for swipe affordance
Accessibility
- Uses React Native
Modalfor proper overlay stacking onRequestClosehandles the Android back buttonstatusBarTranslucentfor full-screen overlay- Backdrop press dismisses the drawer
- Drag handle provides visual swipe affordance
Best Practices
-
Use for contextual actions: Drawers work best for secondary actions, forms, or options that don't need a full screen
-
Keep it focused: Don't overload drawers with too much content — use a full screen instead
-
Always provide a close mechanism: Include a cancel button, backdrop press, or swipe-to-dismiss
-
Footer for actions: Place primary action buttons in
DrawerFooter -
Mobile-first: Drawers are primarily a mobile pattern — consider using
Dialogon larger screens
Related Components
- Use
Dialogfor centered modal content - Use
Sheetfor side-panel overlays - Use
AlertDialogfor confirmation dialogs