Drawer

A panel that slides in from the edge of the screen with swipe-to-dismiss gestures.

import { Button } from "@/components/ui/button/button";
import {
  Drawer,
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-side.module.css";

export default function DrawerSide() {
  return (
    <Drawer swipeDirection="right">
      <DrawerTrigger render={<Button>View Activity</Button>} />
      <DrawerPortal>
        <DrawerBackdrop />
        <DrawerViewport className={styles.viewport}>
          <DrawerPopup className={styles.popup}>
            <DrawerContent>
              <DrawerTitle>Activity</DrawerTitle>
              <DrawerDescription>
                Recent changes across your workspace this week.
              </DrawerDescription>
              <div className={styles.actions}>
                <DrawerClose render={<Button variant="outline" size="sm">Close</Button>} />
              </div>
            </DrawerContent>
          </DrawerPopup>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  );
}

npx shadcn@latest add @roiui/drawer
npx shadcn@latest add @roiui/drawer-tailwind

anatomy
<Drawer>
  <DrawerTrigger />
  <DrawerPortal>
      <DrawerBackdrop />
      <DrawerViewport>
          <DrawerPopup>
              <DrawerHandle />
              <DrawerContent>
                  <DrawerTitle />
                  <DrawerDescription />
                  <DrawerClose />
              </DrawerContent>
          </DrawerPopup>
      </DrawerViewport>
  </DrawerPortal>
</Drawer>

import { Button } from "@/components/ui/button/button";
import {
  Drawer,
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHandle,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-demo.module.css";

export default function DrawerDemo() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button>View Storage</Button>} />
      <DrawerPortal>
        <DrawerBackdrop />
        <DrawerViewport>
          <DrawerPopup className={styles.popup}>
            <DrawerHandle />
            <DrawerContent>
              <DrawerTitle className={styles.title}>Storage</DrawerTitle>
              <DrawerDescription className={styles.description}>
                You&apos;ve used 4.2 GB of 10 GB. Upgrade your plan for more space.
              </DrawerDescription>
              <div className={styles.actions}>
                <DrawerClose render={<Button variant="outline" size="sm">Done</Button>} />
              </div>
            </DrawerContent>
          </DrawerPopup>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  );
}

"use client";

import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
  Drawer,
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHandle,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-nested.module.css";

export default function DrawerNested() {
  const [firstOpen, setFirstOpen] = React.useState(false);
  const [secondOpen, setSecondOpen] = React.useState(false);
  const [thirdOpen, setThirdOpen] = React.useState(false);

  return (
    <Drawer
      open={firstOpen}
      onOpenChange={(nextOpen) => {
        setFirstOpen(nextOpen);
        if (!nextOpen) {
          setSecondOpen(false);
          setThirdOpen(false);
        }
      }}
    >
      <DrawerTrigger render={<Button>Workspace Settings</Button>} />
      <DrawerPortal>
        <DrawerBackdrop />
        <DrawerViewport>
          <DrawerPopup className={styles.popup}>
            <DrawerHandle className={styles.handle} />
            <DrawerContent className={styles.content}>
              <DrawerTitle className={styles.title}>Workspace</DrawerTitle>
              <DrawerDescription className={styles.description}>
                Manage your workspace settings, members, and billing.
              </DrawerDescription>
              <div className={styles.actions}>
                <div className={styles.actionsLeft}>
                  <Drawer
                    open={secondOpen}
                    onOpenChange={(nextOpen) => {
                      setSecondOpen(nextOpen);
                      if (!nextOpen) {
                        setThirdOpen(false);
                      }
                    }}
                  >
                    <DrawerTrigger render={<Button variant="link" size="sm" className={styles.linkButton}>Team members</Button>} />
                    <DrawerPortal>
                      <DrawerViewport>
                        <DrawerPopup className={styles.popup}>
                          <DrawerHandle className={styles.handle} />
                          <DrawerContent className={styles.content}>
                            <DrawerTitle className={styles.title}>Team</DrawerTitle>
                            <DrawerDescription className={styles.description}>
                              Manage who has access to this workspace.
                            </DrawerDescription>
                            <ul className={styles.list}>
                              <li>5 active members</li>
                              <li>2 pending invitations</li>
                              <li>3 roles configured</li>
                            </ul>
                            <div className={styles.actions}>
                              <div className={styles.actionsLeft}>
                                <Drawer open={thirdOpen} onOpenChange={setThirdOpen}>
                                  <DrawerTrigger render={<Button variant="link" className={styles.linkButton}>Invite member</Button>} />
                                  <DrawerPortal>
                                    <DrawerViewport>
                                      <DrawerPopup className={styles.popup}>
                                        <DrawerHandle className={styles.handle} />
                                        <DrawerContent className={styles.content}>
                                          <DrawerTitle className={styles.title}>Invite Member</DrawerTitle>
                                          <DrawerDescription className={styles.description}>
                                            Send an invitation to join your workspace.
                                          </DrawerDescription>
                                          <div className={styles.field}>
                                            <label className={styles.label} htmlFor="invite-email">Email address</label>
                                            <input id="invite-email" className={styles.input} defaultValue="colleague@company.com" />
                                          </div>
                                          <div className={styles.field}>
                                            <label className={styles.label} htmlFor="invite-role">Role</label>
                                            <textarea id="invite-role" className={styles.textarea} defaultValue="Editor" rows={3} />
                                          </div>
                                          <div className={styles.actions}>
                                            <DrawerClose render={<Button variant="outline" size="sm">Send Invite</Button>} />
                                          </div>
                                        </DrawerContent>
                                      </DrawerPopup>
                                    </DrawerViewport>
                                  </DrawerPortal>
                                </Drawer>
                              </div>
                              <DrawerClose render={<Button variant="outline" size="sm">Close</Button>} />
                            </div>
                          </DrawerContent>
                        </DrawerPopup>
                      </DrawerViewport>
                    </DrawerPortal>
                  </Drawer>
                </div>
                <DrawerClose render={<Button variant="outline" size="sm">Close</Button>} />
              </div>
            </DrawerContent>
          </DrawerPopup>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  );
}

"use client";

import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
  Drawer,
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-snap-points.module.css";

const TOP_MARGIN_REM = 1;
const VISIBLE_SNAP_POINTS_REM = [30];

function toViewportSnapPoint(heightRem: number) {
  return `${heightRem + TOP_MARGIN_REM}rem`;
}

const snapPoints = [...VISIBLE_SNAP_POINTS_REM.map(toViewportSnapPoint), 1];

export default function DrawerSnapPoints() {
  return (
    <Drawer snapPoints={snapPoints}>
      <DrawerTrigger render={<Button>Explore</Button>} />
      <DrawerPortal>
        <DrawerBackdrop />
        <DrawerViewport className={styles.viewport}>
          <DrawerPopup
            className={styles.popup}
            style={{ "--top-margin": `${TOP_MARGIN_REM}rem` } as React.CSSProperties}
          >
            <div className={styles.dragArea}>
              <div className={styles.handle} />
              <DrawerTitle className={styles.title}>Discover</DrawerTitle>
            </div>
            <DrawerContent className={styles.scroll}>
              <div className={styles.content}>
                <DrawerDescription className={styles.description}>
                  Trending topics and curated picks based on your interests.
                </DrawerDescription>
                <div className={styles.cards} aria-hidden>
                  {Array.from({ length: 20 }, (_, index) => (
                    <div className={styles.card} key={index} />
                  ))}
                </div>
                <div className={styles.actions}>
                  <DrawerClose render={<Button variant="outline" size="sm">Close</Button>} />
                </div>
              </div>
            </DrawerContent>
          </DrawerPopup>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  );
}

"use client";

import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHandle,
  DrawerIndent,
  DrawerIndentBackground,
  DrawerPopup,
  DrawerPortal,
  DrawerProvider,
  DrawerRoot,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-indent.module.css";

export default function DrawerIndentDemo() {
  const [portalContainer, setPortalContainer] = React.useState<HTMLDivElement | null>(null);

  return (
    <DrawerProvider>
      <div className={styles.root} ref={setPortalContainer}>
        <DrawerIndentBackground className={styles.indentBackground} />
        <DrawerIndent className={styles.indent}>
          <div className={styles.center}>
            <DrawerRoot modal={false}>
              <DrawerTrigger render={<Button>New Note</Button>} />
              <DrawerPortal container={portalContainer}>
                <DrawerBackdrop className={styles.backdrop} />
                <DrawerViewport className={styles.viewport}>
                  <DrawerPopup>
                    <DrawerHandle />
                    <DrawerContent>
                      <DrawerTitle className={styles.title}>Quick Note</DrawerTitle>
                      <DrawerDescription className={styles.description}>
                        Capture a thought before it slips away.
                      </DrawerDescription>
                      <div className={styles.actions}>
                        <DrawerClose render={<Button variant="outline" size="sm">Done</Button>} />
                      </div>
                    </DrawerContent>
                  </DrawerPopup>
                </DrawerViewport>
              </DrawerPortal>
            </DrawerRoot>
          </div>
        </DrawerIndent>
      </div>
    </DrawerProvider>
  );
}

import { Button } from "@/components/ui/button/button";
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-non-modal.module.css";

export default function DrawerNonModal() {
  return (
    <Drawer swipeDirection="right" modal={false} disablePointerDismissal>
      <DrawerTrigger render={<Button>Start Export</Button>} />
      <DrawerPortal>
        <DrawerViewport className={styles.viewport}>
          <DrawerPopup className={styles.popup}>
            <DrawerContent>
              <DrawerTitle>Export Complete</DrawerTitle>
              <DrawerDescription>
                Your file has been saved to the Downloads folder. You can continue working while it
                processes.
              </DrawerDescription>
              <div className={styles.actions}>
                <DrawerClose render={<Button variant="outline" size="sm">Dismiss</Button>} />
              </div>
            </DrawerContent>
          </DrawerPopup>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  );
}

"use client";

import * as React from "react";
import Link from "next/link";
import { ScrollArea } from "@base-ui/react/scroll-area";
import { Button } from "@/components/ui/button/button";
import {
  Drawer,
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHandle,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-mobile-nav.module.css";

const ITEMS = [
  { href: "#", label: "Home" },
  { href: "#", label: "Products" },
  { href: "#", label: "Pricing" },
  { href: "#", label: "About" },
] as const;

const LONG_LIST = Array.from({ length: 50 }, (_, i) => ({
  href: "#",
  label: `Page ${i + 1}`,
}));

export default function DrawerMobileNav() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button>Open Menu</Button>} />
      <DrawerPortal>
        <DrawerBackdrop className={styles.backdrop} />
        <DrawerViewport className={styles.viewport}>
          <ScrollArea.Root style={{ position: undefined }} className={styles.scrollAreaRoot}>
            <ScrollArea.Viewport className={styles.scrollAreaViewport}>
              <ScrollArea.Content className={styles.scrollContent}>
                <DrawerPopup className={styles.popup}>
                  <nav aria-label="Navigation" className={styles.panel}>
                    <div className={styles.header}>
                      <div aria-hidden className={styles.headerSpacer} />
                      <DrawerHandle className={styles.handle} />
                      <DrawerClose aria-label="Close menu" className={styles.closeButton}>
                        <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
                          <path
                            d="M0.75 0.75L6 6M11.25 11.25L6 6M6 6L0.75 11.25M6 6L11.25 0.75"
                            stroke="currentcolor"
                            strokeWidth="2"
                            strokeLinecap="round"
                            strokeLinejoin="round"
                          />
                        </svg>
                      </DrawerClose>
                    </div>

                    <DrawerContent className={styles.content}>
                      <DrawerTitle className={styles.title}>Navigation</DrawerTitle>
                      <DrawerDescription className={styles.description}>
                        Browse all sections. Pull down to dismiss.
                      </DrawerDescription>

                      <div className={styles.scrollArea}>
                        <ul className={styles.list}>
                          {ITEMS.map((item) => (
                            <li key={item.label} className={styles.item}>
                              <Link className={styles.link} href={item.href}>
                                {item.label}
                              </Link>
                            </li>
                          ))}
                        </ul>

                        <ul className={styles.longList} aria-label="Long list">
                          {LONG_LIST.map((item) => (
                            <li key={item.label} className={styles.item}>
                              <Link className={styles.link} href={item.href}>
                                {item.label}
                              </Link>
                            </li>
                          ))}
                        </ul>
                      </div>
                    </DrawerContent>
                  </nav>
                </DrawerPopup>
              </ScrollArea.Content>
            </ScrollArea.Viewport>
            <ScrollArea.Scrollbar className={styles.scrollbar}>
              <ScrollArea.Thumb className={styles.scrollbarThumb} />
            </ScrollArea.Scrollbar>
          </ScrollArea.Root>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  );
}

"use client";

import * as React from "react";
import { Button } from "@/components/ui/button/button";
import {
  Drawer,
  DrawerBackdrop,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerTrigger,
  DrawerViewport,
} from "@/components/ui/drawer/drawer";
import styles from "./drawer-action-sheet.module.css";

const ACTIONS = ["Share", "Duplicate", "Move to Folder", "Pin to Top", "Print"];

export default function DrawerActionSheet() {
  const [open, setOpen] = React.useState(false);

  return (
    <Drawer open={open} onOpenChange={setOpen}>
      <DrawerTrigger render={<Button>More Actions</Button>} />
      <DrawerPortal>
        <DrawerBackdrop className={styles.backdrop} />
        <DrawerViewport>
          <DrawerPopup className={styles.popup}>
            <DrawerContent className={styles.surface}>
              <DrawerTitle className={styles.visuallyHidden}>File actions</DrawerTitle>
              <DrawerDescription className={styles.visuallyHidden}>
                Choose an action for this file.
              </DrawerDescription>

              <ul className={styles.actions} aria-label="File actions">
                {ACTIONS.map((action, index) => (
                  <li key={action} className={styles.action}>
                    {index === 0 && (
                      <DrawerClose className={styles.visuallyHidden}>
                        Close file actions
                      </DrawerClose>
                    )}
                    <button
                      type="button"
                      className={styles.actionButton}
                      onClick={() => setOpen(false)}
                    >
                      {action}
                    </button>
                  </li>
                ))}
              </ul>
            </DrawerContent>
            <div className={styles.dangerSurface}>
              <button
                type="button"
                className={styles.dangerButton}
                onClick={() => setOpen(false)}
              >
                Delete
              </button>
            </div>
          </DrawerPopup>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  );
}