AniUI

Drawer

A sliding panel that emerges from the left or right edge of the screen, ideal for navigation menus and side content.

Requires react-native-reanimated (Tier 2).

Web preview — components render natively on iOS & Android
import { Drawer, DrawerContent } from "@/components/ui/drawer";

export function MyScreen() {
  const [open, setOpen] = React.useState(false);
  return (
    <>
      <Button onPress={() => setOpen(true)}>Open Drawer</Button>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent>
          <Text>Drawer content here</Text>
        </DrawerContent>
      </Drawer>
    </>
  );
}

Installation#

npx @aniui/cli add drawer

Usage#

app/index.tsx
import { Drawer, DrawerContent } from "@/components/ui/drawer";

export function MyScreen() {
  const [open, setOpen] = React.useState(false);
  return (
    <>
      <Button onPress={() => setOpen(true)}>Open Drawer</Button>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent>
          <Text>Drawer content here</Text>
        </DrawerContent>
      </Drawer>
    </>
  );
}

Left Drawer (Default)#

Web preview — components render natively on iOS & Android
<Drawer open={open} onOpenChange={setOpen} side="left">
  <DrawerContent>
    <Text>Left drawer content</Text>
  </DrawerContent>
</Drawer>

Right Drawer#

Web preview — components render natively on iOS & Android
<Drawer open={open} onOpenChange={setOpen} side="right">
  <DrawerContent>
    <Text>Right drawer content</Text>
  </DrawerContent>
</Drawer>

Props#

Drawer#

PropTypeDefault
open
boolean
required
onOpenChange
(open: boolean) => void
required
side
"left" | "right"
"left"
children
ReactNode
required

DrawerContent#

PropTypeDefault
className
string
children
ReactNode

DrawerContent also accepts all View props from React Native.

Accessibility#

  • Side drawer with accessibilityRole="menu".
  • Backdrop dismiss and close button are accessible to screen readers.

Source#

components/ui/drawer.tsx
import React, { useEffect, useState } from "react";
import { View, Pressable, Modal } from "react-native";
import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming } from "react-native-reanimated";
import { springs, duration } from "@/components/ui/animate";
import { cn } from "@/lib/utils";

export interface DrawerProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  side?: "left" | "right";
  children: React.ReactNode;
}

export function Drawer({ open, onOpenChange, side = "left", children }: DrawerProps) {
  // Keep the Modal mounted while the slide-out plays; unmounting on `open`
  // directly would cut the close animation short.
  const [visible, setVisible] = useState(open);
  const translate = useSharedValue(side === "left" ? -300 : 300);
  const opacity = useSharedValue(0);

  useEffect(() => {
    if (open) {
      setVisible(true);
      translate.value = withSpring(0, springs.snappy);
      opacity.value = withTiming(0.5, { duration: duration.normal });
      return;
    }
    translate.value = withTiming(side === "left" ? -300 : 300, { duration: duration.normal });
    opacity.value = withTiming(0, { duration: duration.normal });
    const t = setTimeout(() => setVisible(false), duration.normal);
    return () => clearTimeout(t);
  }, [open, side, translate, opacity]);

  const overlayStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
  const drawerStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translate.value }],
  }));

  const close = () => onOpenChange(false);

  return (
    <Modal visible={visible} transparent animationType="none" onRequestClose={close}>
      <Pressable style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }} onPress={close} accessible={false}>
        <Animated.View style={[{ flex: 1, backgroundColor: "#000000" }, overlayStyle]} />
      </Pressable>
      <Animated.View
        style={[
          {
            position: "absolute",
            top: 0,
            bottom: 0,
            width: 288,
            ...(side === "left" ? { left: 0 } : { right: 0 }),
          },
          drawerStyle,
        ]}
      >
        <View className={cn("flex-1 bg-card", side === "left" ? "border-r border-border" : "border-l border-border")} accessibilityRole="menu">
          {children}
        </View>
      </Animated.View>
    </Modal>
  );
}

export interface DrawerContentProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  children?: React.ReactNode;
}

export function DrawerContent({ className, ...props }: DrawerContentProps) {
  return <View className={cn("flex-1 p-4", className)} {...props} />;
}