Drawer

Free · MITReact Native · Expo

A 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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/drawer.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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).
booleanfalse
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.
number24
insets
Pad the drawer by the safe area. Turn off when the Drawer is not full screen. Default `true`.
booleantrue
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.