Drawer
Free · MITReact Native · ExpoA side drawer that follows the finger from the screen edge: the scrim darkens with its progress and, in the card variant, the screen behind shrinks, rounds and slides aside on the same progress. Release settles it with the finger's velocity; Escape, the back button or a tap on the scrim closes it.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/drawer.jsonThen run npx expo install --fix so react-native-gesture-handler, react-native-reanimated match your Expo SDK. Files land in components/goodui-native/drawer.
Usage
import { Pressable, Text, View } from "react-native";
import { Drawer, useDrawer } from "@/components/goodui-native/drawer/drawer";
function Menu() {
const { close } = useDrawer();
return (
<View style={{ padding: 16, gap: 12 }}>
<Pressable accessibilityRole="button" onPress={close}>
<Text>Inbox</Text>
</Pressable>
</View>
);
}
function Inbox() {
const { toggle } = useDrawer();
return (
<Pressable accessibilityRole="button" onPress={toggle}>
<Text>Open menu</Text>
</Pressable>
);
}
export function MailScreen() {
return (
<Drawer variant="card" accessibilityLabel="Mailboxes" content={<Menu />}>
<Inbox />
</Drawer>
);
}When to use
- App navigation with more destinations than a tab bar holds: mailboxes, workspaces, accounts.
- A filter or settings panel beside a list or map on a wide or busy screen.
- An account or workspace switcher that should stay one swipe away.
- Three to five top-level destinations: use tab-bar, which keeps them visible.
- A short list of actions for one item: use action-sheet or context-menu.
- Content that comes up from the bottom or needs detents: use bottom-sheet.
- Switching between views of the same content: use segmented-control or top-tabs.
Examples
Controlled filter drawer from the right
const [open, setOpen] = useState(false);
<Drawer side="right" width={300} open={open} onOpenChange={setOpen} accessibilityLabel="Filters" content={<Filters onApply={() => setOpen(false)} />}>
<Results onFilters={() => setOpen(true)} />
</Drawer>Embedded in part of the screen
<View style={{ height: 480, borderRadius: 20, overflow: "hidden" }}>
<Drawer insets={false} content={<Menu />}>
<Preview />
</Drawer>
</View>API reference
| Prop | Type | Default |
|---|---|---|
| children* The screen the drawer opens over. Fills the Drawer, which fills its parent. | ReactNode | — |
| content* What the drawer shows: navigation, filters, account switcher. It mounts when the drawer starts to open. | ReactNode | — |
| open Open state (controlled). Pair it with `onOpenChange`. | boolean | — |
| defaultOpen Open state on first render (uncontrolled). | boolean | false |
| onOpenChange Called when a swipe, a tap on the scrim, Escape or the back button opens or closes it. | (open: boolean) => void | — |
| side Edge the drawer comes from. Default `start`: the left, or the right under RTL. | "end" | "left" | "right" | "start" | "start" |
| variant `overlay` slides the drawer over the screen above a fading scrim. `card` slides the screen aside as a shrinking, rounding card and reveals the drawer underneath. | "overlay" | "card" | "overlay" |
| width Drawer width in points. Default 82 % of the Drawer's width, at most 320. | number | — |
| edgeWidth Width of the strip at the edge that starts an open swipe. Default 24. `0` turns edge swipes off. | number | 24 |
| insets Pad the drawer by the safe area. Turn off when the Drawer is not full screen. Default `true`. | boolean | true |
| haptic Haptic when a swipe or a tap commits open or closed. `false` turns it off. | HapticName | false | "tap" |
| accessibilityLabel Screen reader name of the drawer. Same as `labels.menu`. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<DrawerLabels> | — |
| style Style of the outer container. | StyleProp<ViewStyle> | — |
| panelStyle Style of the drawer surface, for example a different background. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- One progress value drives everything: the drawer's position (1:1 with the finger), the scrim's opacity and, in the card variant, the screen's scale (to 0.86), corner radius (to radius.xl) and slide.
- Release projects the velocity: past halfway (with a flick counted in) it opens, otherwise it closes, on the smooth spring carrying the finger's velocity.
- Overlay: the screen behind shifts 10 % of the drawer width as the drawer arrives. Card: the drawer underneath eases in from behind and fades up.
- Reduced motion: opening and closing from a button fade the drawer and scrim instead of sliding; a swipe still follows the finger and settles with a short timing; no parallax.
- Haptic: tap when a swipe or a scrim tap commits open or closed (configurable with haptic); none for programmatic changes
- All motion runs on the UI thread from a single shared value; React state changes only when the drawer commits or finishes closing.
- Drawer content mounts when the drawer starts opening and unmounts after it closes.
Accessibility
- The open drawer is a modal dialog named by accessibilityLabel (accessibilityViewIsModal on iOS, role dialog with aria-modal on the web); the screen behind is hidden from screen readers while it is open.
- The scrim is a button named 'Close <label>' with activate and escape actions; Escape closes on the web and the back button closes on Android.
- Opening and closing are announced ('Menu opened').
- The closed drawer's content is unmounted, so it is never reachable by keyboard or screen reader; the edge strip is hidden from them.
Notes for AI
- Drawer wraps the screen: children is the screen, content is the drawer. It fills its parent (flex: 1); give an embedded one a height.
- Inside either side, call useDrawer() for open, setOpen, toggle and close. A menu item should close() after navigating.
- Pass insets={false} when the Drawer is not full screen, or the drawer gets safe-area padding it does not need.
- Drawer content sits on the background color (overlay) or a quiet surfaceMuted fill (card); put ListSection and ListRow in it for a native menu.
- Do not wrap the whole app's navigator in a Drawer just for one screen; scope it to the screens that have the menu.
- edgeWidth={0} turns off the edge swipe, for example on a screen with its own horizontal gestures at the edge.
Related
A bottom tab bar whose pill stretches toward the next tab and can be dragged to scrub between tabs; the selected icon bounces, badges pop, and it can hide while scrolling.
A sheet that rests at detents, follows the finger 1:1, settles with velocity and hands drags off to its scroll view.
Settings and navigation cells with a token-colored icon tile, title, subtitle and a trailing value, switch, badge, check or chevron, grouped in inset sections with headers and footers. On touch down the highlight floods in, the tile sinks and the chevron leans forward; on release the highlight lingers and fades.
A swipeable pager under a tab strip whose indicator tracks the swipe continuously: the leading edge runs ahead and the trailing edge follows, so it stretches between tabs and gathers as the page lands. Labels recolor with the swipe and a scrolling strip keeps the indicator centred.