WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/drawer.json

Playground

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

<Drawer />

Installation

npx shadcn@latest add https://webberui.com/r/drawer.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>;

Props

Drawer

PropTypeDefaultDescription
childrenReact.ReactNodeContent; must include DrawerTrigger and DrawerContent
openbooleanOpen state for controlled mode; when omitted, the component manages it internally
onOpenChange(open: boolean) => voidCallback fired when the open state changes (in both controlled and uncontrolled modes)

DrawerTrigger

PropTypeDefaultDescription
childrenReact.ReactNodeTrigger content
classNamestringAppended to the trigger className

DrawerContent

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""right"The direction the drawer slides in from
classNamestringAppended to the panel container className
childrenReact.ReactNodePanel content

DrawerClose

PropTypeDefaultDescription
childrenReact.ReactNodelucide-react X iconCustom button content
classNamestringAppended 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

On this page