How to make a side drawer menu in React Native

By the LumioUI team · Updated

A side drawer is a menu that slides in from the edge over a dimmed backdrop. To build one in React Native, animate the panel's translateX with Reanimated and fade the backdrop from the same value. Close it on a backdrop tap or a swipe. The LumioUI Drawer does this with rows that highlight when tapped, then close.

Live demo of the LumioUI Drawer component. It's interactive, so try it.

How a drawer menu works

  1. One value drives everything. The panel's offset runs from its negative width (hidden) to 0 (shown). The backdrop's opacity is derived from it, so the dim follows your finger exactly.
  2. Swipe to close. A horizontal pan with an activation offset, so vertical scrolling inside still works. A flick or dragging past a third closes it.
  3. Above everything. Render the drawer at the root, or in a portal, so it covers headers and tab bars.
  4. Accessibility. Mark the panel modal (accessibilityViewIsModal) so screen readers stay inside it. Label the backdrop "Close menu", and give the active row selected state.
TSX SideDrawer.tsx
import * as React from "react";
import { Pressable, StyleSheet, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring, withTiming } from "react-native-reanimated";

const W = 300;

export function SideDrawer({ open, onClose, children }) {
  const x = useSharedValue(-W); // -W hidden, 0 shown

  React.useEffect(() => {
    x.value = open ? withSpring(0, { damping: 24, stiffness: 240 }) : withTiming(-W, { duration: 180 });
  }, [open]);

  const swipe = Gesture.Pan()
    .activeOffsetX([-10, 10])
    .onChange((e) => { x.value = Math.min(0, x.value + e.changeX); })
    .onEnd((e) => {
      if (e.velocityX < -500 || x.value < -W / 3) runOnJS(onClose)();
      else x.value = withSpring(0);
    });

  const panel = useAnimatedStyle(() => ({ transform: [{ translateX: x.value }] }));
  const dim = useAnimatedStyle(() => ({ opacity: 0.4 * (1 + x.value / W) }));

  return (
    <View style={StyleSheet.absoluteFill} pointerEvents={open ? "auto" : "none"}>
      <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: "#000" }, dim]}>
        <Pressable style={{ flex: 1 }} onPress={onClose} accessibilityLabel="Close menu" />
      </Animated.View>
      <GestureDetector gesture={swipe}>
        <Animated.View style={[{ position: "absolute", top: 0, bottom: 0, width: W, backgroundColor: "#fff" }, panel]}
          accessibilityViewIsModal>
          {children}
        </Animated.View>
      </GestureDetector>
    </View>
  );
}

Using React Navigation? Its drawer navigator handles routing. You can still render your own drawer content inside it.

The quick way: LumioUI Drawer

The Drawer slides in over a dimmed backdrop. Its rows can appear in turn and show rolling unread counts. Tapping a row highlights it and closes the drawer, or you can swipe it away. It's free and runs in Expo Go.

TSX AppMenu.tsx
import * as React from "react";
import { Drawer } from "@/components/ui/drawer/drawer";
import { Icon } from "@/components/ui/icon/icon";

export function AppMenu({ open, onOpenChange, route, go }) {
  return (
    <Drawer open={open} onOpenChange={onOpenChange} side="left" width={300}>
      <Drawer.Header>
        <Drawer.Title>Maya Chen</Drawer.Title>
      </Drawer.Header>
      <Drawer.Item label="Inbox" icon={<Icon name="mail" />} active={route === "inbox"} trailing="12" onPress={() => go("inbox")} />
      <Drawer.Item label="Starred" icon={<Icon name="star" />} active={route === "starred"} onPress={() => go("starred")} />
      <Drawer.Item label="Settings" icon={<Icon name="settings" />} active={route === "settings"} onPress={() => go("settings")} />
    </Drawer>
  );
}
  • open and onOpenChange are required (it's controlled). side is "left" or "right", and width defaults to 300.
  • Drawer.Item takes label, icon, active, trailing (a count or a chevron) and onPress.
  • On tablets, prefer a Navigation Rail (Pro), which stays on screen and expands into a sidebar.

Add it to your app

LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:

npx @lumioui/cli init
npx @lumioui/cli add drawer

It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Drawer API.

More React Native guides