Overlays & Feedback

Drawer

A panel that slides in from the edge of the screen, supporting all four directions, click-the-overlay to close, and single-axis drag to swipe it away.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add drawer.en

New project or installation trouble? Start here →

Drawer is a React drawer and side panel component built with Motion (motion/react) and Tailwind CSS. It slides in from the top, right, bottom or left, which also makes it a natural mobile bottom sheet. The panel is portaled to document.body and can be dragged away along its closing direction.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<Drawer/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • A hamburger menu or category navigation on a mobile site, sliding in from the left or right
  • A filter panel on an e-commerce listing page that closes and refreshes results when applied
  • A mobile bottom sheet for actions such as share, sort or picking a product variant
  • Opening record details or a quick-edit form from the right after clicking a row in an admin table
  • Not a good fit: for an inspector that stays docked beside the main content instead of overlaying it, use Docked Inspector Rail

Installation

npx webberui@latest add https://webberui.com/r/drawer.en.json

Or, once registries are configured in components.json, install it as @webberui/drawer.

Usage

import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerClose,
} from "@/components/ui/drawer";

<Drawer>
  <DrawerTrigger>Open menu</DrawerTrigger>

  <DrawerContent side="right" className="w-72">
    <div className="flex items-center justify-between border-b border-neutral-200 px-5 py-4 dark:border-neutral-800">
      <h2 className="text-base font-semibold">Menu</h2>
      <DrawerClose />
    </div>
    <nav className="p-3">{/* items */}</nav>
  </DrawerContent>
</Drawer>;

DrawerContent's side decides the direction it slides in from ("top" | "right" | "bottom" | "left", default "right"). The panel slides in from the matching edge with a spring, and clicking the overlay or pressing Esc closes it.

Pass open and onOpenChange to switch to controlled mode:

const [open, setOpen] = React.useState(false);

<Drawer open={open} onOpenChange={setOpen}>
  {/* ... */}
</Drawer>;

Examples

Mobile bottom sheet

side="bottom" pins the panel to the bottom edge with a 85vh max height; add rounded corners and a grab-handle hint through className.

import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerClose,
} from "@/components/ui/drawer";

<Drawer>
  <DrawerTrigger>Choose size</DrawerTrigger>
  <DrawerContent side="bottom" className="rounded-t-2xl">
    <div className="mx-auto mt-3 h-1.5 w-10 rounded-full bg-neutral-300 dark:bg-neutral-700" />
    <div className="flex items-center justify-between px-5 py-4">
      <h2 className="text-base font-semibold">Choose size</h2>
      <DrawerClose />
    </div>
    <div className="grid grid-cols-3 gap-2 px-5 pb-6">
      {["S", "M", "L"].map((size) => (
        <button key={size} className="rounded-lg border py-2 text-sm">
          {size}
        </button>
      ))}
    </div>
  </DrawerContent>
</Drawer>;

Filter panel that closes on apply

In controlled mode, set open back to false yourself when the form is submitted.

"use client";

import * as React from "react";
import { Drawer, DrawerTrigger, DrawerContent } from "@/components/ui/drawer";

export function FilterDrawer({ onApply }: { onApply: (q: FormData) => void }) {
  const [open, setOpen] = React.useState(false);

  return (
    <Drawer open={open} onOpenChange={setOpen}>
      <DrawerTrigger>Filters</DrawerTrigger>
      <DrawerContent side="left">
        <form
          className="flex flex-1 flex-col gap-4 p-5"
          onSubmit={(e) => {
            e.preventDefault();
            onApply(new FormData(e.currentTarget));
            setOpen(false);
          }}
        >
          <label className="flex items-center gap-2 text-sm">
            <input type="checkbox" name="inStock" /> In stock only
          </label>
          <button type="submit" className="mt-auto rounded-lg bg-neutral-900 py-2 text-sm text-white">
            Apply filters
          </button>
        </form>
      </DrawerContent>
    </Drawer>
  );
}

Custom close button content

DrawerClose shows an X icon by default; pass children to turn it into a text button.

<DrawerContent side="right">
  <div className="p-5">{/* order summary */}</div>
  <div className="mt-auto border-t p-4">
    <DrawerClose className="w-full py-2 text-sm">Continue shopping</DrawerClose>
  </div>
</DrawerContent>

Props

Drawer

PropTypeDefaultDescription
childrenReact.ReactNode—Content; must include DrawerTrigger and DrawerContent
openboolean—Open state for controlled mode; when omitted, the component manages it internally
onOpenChange(open: boolean) => void—Callback fired when the open state changes (in both controlled and uncontrolled modes)

DrawerTrigger

PropTypeDefaultDescription
childrenReact.ReactNode—Trigger content
classNamestring—Appended to the trigger className

DrawerContent

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""right"The direction the drawer slides in from
classNamestring—Appended to the panel container className
childrenReact.ReactNode—Panel content

DrawerClose

PropTypeDefaultDescription
childrenReact.ReactNodelucide-react X iconCustom button content
classNamestring—Appended to the close button className

How it works

  • Four directions: side maps to the edge the panel snaps to and its initial off-screen offset. Left and right render as a full-height sidebar (w-80 max-w-[85vw]), top and bottom as a full-width banner (max-h-[85vh]); both sizes can be overridden with className.
  • Drag to close: the panel tracks the pointer with single-axis drag, releasing the constraint only in the "closing" direction while the opening direction springs back with elastic damping. On release, a displacement past 80px or a velocity past 500px/s in the closing direction closes it, and AnimatePresence's exit continues the slide-out from the current drag position; below the threshold, dragSnapToOrigin springs it back to place.
  • Portal to body: the panel and the overlay are rendered into document.body via createPortal, so an ancestor's overflow clipping or a transform establishing a containing block cannot affect fixed positioning.
  • Spring slide-in: the entrance uses a spring (stiffness: 380, damping: 40), the same spring used by the drag spring back, so gestures and programmatic animation settle with the same feel.
  • Controlled mode: passing open switches to controlled mode and the component stops touching its internal state; onOpenChange fires in both controlled and uncontrolled modes, so it can be used to observe open/close events.
  • Tuning points: the color transition duration reads var(--wb-duration-fast, 200ms), falling back to 200ms when undefined.

Accessibility

  • The panel carries role="dialog" and aria-modal="true"; focus moves into the panel on open (tabIndex={-1}), and focus returns to the trigger after close
  • The trigger is a native <button> carrying aria-haspopup="dialog" and an aria-expanded state, linked to the panel with aria-controls while open
  • Esc or a click on the overlay closes it; while open, body is set to overflow: hidden to lock background scrolling and restored to its previous value on close
  • When the user has "reduce motion" enabled at the system level, the slide-in and the drag degrade to a quick fade in / fade out with no sense of movement
  • The component takes the lightweight route and does not build in a focus trap; if the panel holds several interactive elements and needs a complete keyboard focus cycle, pair it with a focus trap solution of your own

FAQ

Does it work with the Next.js App Router?

Yes. The file already starts with "use client", so import it from a Server Component page as usual. DrawerContent only creates its portal after mounting, so the server renders nothing for it and there is no hydration mismatch.

How do I change the panel width or height?

Pass className to DrawerContent. Left and right panels default to w-80 max-w-[85vw], top and bottom panels to max-h-[85vh]; override them with Tailwind classes such as className="w-96".

How far do I have to drag to close it?

It closes when the drag passes 80px in the closing direction or the release velocity exceeds 500px/s; otherwise it springs back. When the OS "reduce motion" setting is on, dragging is disabled and the panel simply fades.

What keyboard and screen reader support does it have?

The panel has role="dialog" and aria-modal="true", receives focus on open, returns focus to the trigger on close, and closes on Esc. There is no built-in focus trap, so add one yourself if the panel holds many interactive elements.

What are the dependencies?

motion, lucide-react, clsx and tailwind-merge. If you skip the CLI, the Manual tab in the installation section shows how to get the source.