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.
npx shadcn@latest add https://webberui.com/r/drawer.jsonPlayground
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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Content; must include DrawerTrigger and DrawerContent |
open | boolean | — | 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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Trigger content |
className | string | — | Appended to the trigger className |
DrawerContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "right" | The direction the drawer slides in from |
className | string | — | Appended to the panel container className |
children | React.ReactNode | — | Panel content |
DrawerClose
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | lucide-react X icon | Custom button content |
className | string | — | Appended to the close button className |
How it works
- Four directions:
sidemaps 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 withclassName. - 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, andAnimatePresence'sexitcontinues the slide-out from the current drag position; below the threshold,dragSnapToOriginsprings it back to place. - Portal to body: the panel and the overlay are rendered into
document.bodyviacreatePortal, so an ancestor'soverflowclipping or atransformestablishing a containing block cannot affectfixedpositioning. - 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
openswitches to controlled mode and the component stops touching its internal state;onOpenChangefires 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"andaria-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>carryingaria-haspopup="dialog"and anaria-expandedstate, linked to the panel witharia-controlswhile open - Esc or a click on the overlay closes it; while open,
bodyis set tooverflow: hiddento 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
Morphing Dialog
Click a card and it scales and morphs seamlessly into a centered dialog with a shared-element animation, then shrinks back to the original card position on close.
Dynamic Island Toast
An iOS Dynamic Island-style notification — an idle pill expands fluidly into a notification card, multiple notifications stack behind it with their edges peeking out, and a loading state can update in place to success.