Drawer
A slide-in panel component with multiple placement options
Installation
Usage
Basic Usage
A basic drawer that slides in from the right by default.
"use client";
import React, { useState } from "react";
import { Button } from "@/ui";
import { Drawer } from "@/ui";
export default function DrawerBasicDemo() {
const [open, setOpen] = useState(false);
const handleBackdropClick = () => {
setOpen(false);
};
return (
<div className="flex flex-col items-center gap-4">
<Button onClick={() => setOpen(true)}>Open Drawer</Button>
<Drawer
open={open}
onOpenChange={setOpen}
placement="right"
onBackdropClick={handleBackdropClick}
>
<div className="flex flex-col gap-6 pt-4">
<div className="flex flex-col gap-2">
<h3 className="text-lg font-semibold">Drawer</h3>
<p className="text-sm text-muted-foreground">
A slide-in panel component supporting four placement positions, built on native HTML
dialog element. Includes focus trap, keyboard navigation and backdrop click close
behavior.
</p>
</div>
<Button className="w-full" onClick={() => setOpen(false)}>
Close
</Button>
</div>
</Drawer>
</div>
);
}
Resizable Drawer
Drag the handle to resize the drawer; it closes automatically when shrunk below a threshold. Dragging is an advanced composition capability provided by the external useDrag hook.
"use client";
import { useState, useRef } from "react";
import { Button, Drawer, useDrag } from "@/ui";
export default function DrawerResizableDemo() {
const [open, setOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const dragStartRef = useRef({ base: 0, minPx: 0 });
const handleBackdropClick = () => {
setOpen(false);
};
const drag = useDrag({
disabled: !open,
onDragStart: () => {
const el = wrapperRef.current;
if (!el) return;
dragStartRef.current.base = el.offsetHeight;
const computed = getComputedStyle(el);
dragStartRef.current.minPx = parseFloat(computed.minHeight) || 80;
},
onDragMove(info) {
const el = wrapperRef.current;
if (!el) return;
const nextHeight = dragStartRef.current.base - info.dy;
el.style.height = `${nextHeight}px`;
},
onDragEnd(info) {
const el = wrapperRef.current;
if (!el) return;
const finalHeight = dragStartRef.current.base - info.dy;
if (finalHeight <= dragStartRef.current.minPx) {
setOpen(false);
}
},
});
return (
<div className="p-8">
<Button onClick={() => setOpen(true)}>Open Drawer</Button>
<Drawer
ref={wrapperRef}
open={open}
onOpenChange={setOpen}
onBackdropClick={handleBackdropClick}
placement="bottom"
drag={drag}
>
<div className="flex flex-col gap-4 pt-2">
<h3 className="text-lg font-semibold">Bottom placement</h3>
<p>Drag upward: expand, capped by max-height</p>
<p>Drag downward: shrink. Close drawer when calculated size drops below min-height</p>
<Button onClick={() => setOpen(false)}>Close</Button>
</div>
</Drawer>
</div>
);
}
Custom
Assemble the composable parts with your own open/close lifecycle and drag resizing.
"use client";
import { useState, useRef, useEffect } from "react";
import { DrawerRoot, DrawerWrapper, DrawerDrag, DrawerContent, useDrag } from "@/ui";
export default function Demo() {
const [open, setOpen] = useState(false);
const dialogRef = useRef<HTMLDialogElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const dragStartRef = useRef({ baseSize: 0 });
useEffect(() => {
const dialog = dialogRef.current;
const wrapper = wrapperRef.current;
if (!dialog || !wrapper) return;
if (open) {
if (!dialog.open) dialog.showModal();
requestAnimationFrame(() => {
wrapper.style.transform = "translate(0, 0)";
});
} else {
wrapper.style.transform = "translateY(100%)";
}
}, [open]);
useEffect(() => {
const dialog = dialogRef.current;
const wrapper = wrapperRef.current;
if (!dialog || !wrapper) return;
const onTransitionEnd = (e: TransitionEvent) => {
if (e.propertyName === "transform" && !open && dialog.open) {
dialog.close();
}
};
wrapper.addEventListener("transitionend", onTransitionEnd);
return () => wrapper.removeEventListener("transitionend", onTransitionEnd);
}, [open]);
const drag = useDrag({
disabled: !open,
onDragStart: () => {
if (!wrapperRef.current) return;
dragStartRef.current.baseSize = wrapperRef.current.offsetHeight;
},
onDragMove: (info) => {
const el = wrapperRef.current;
if (!el) return;
el.style.transitionDuration = "0ms";
el.style.height = `${dragStartRef.current.baseSize - info.dy}px`;
},
onDragEnd: () => {
if (!wrapperRef.current) return;
wrapperRef.current.style.transitionDuration = "";
},
});
return (
<div className="flex flex-col gap-4 items-center p-6">
<button onClick={() => setOpen(true)}>Open Drawer</button>
<DrawerRoot
ref={dialogRef}
onCancel={(e) => {
e.preventDefault();
setOpen(false);
}}
onClick={(e) => {
if (e.target === e.currentTarget) setOpen(false);
}}
>
<DrawerWrapper
ref={wrapperRef}
isHorizontal={false}
placement="bottom"
style={{ transform: "translateY(100%)" }}
className="h-[30vh] min-h-[15vh] max-h-[85vh]"
>
<DrawerDrag isHorizontal={false} onPointerDown={drag.handlePointerDown} />
<DrawerContent className="bg-background">
<p>Drag handle to shrink / expand panel</p>
<button className="mt-2" onClick={() => setOpen(false)}>
Close
</button>
</DrawerContent>
</DrawerWrapper>
</DrawerRoot>
</div>
);
}
API Reference
High-level Components
Ready-to-use composite components.
Drawer
A slide-in panel component with focus management and keyboard navigation.
| Prop | Type | Default | Description |
|---|---|---|---|
ref | React.Ref<HTMLDivElement> | - | Forwarded to DrawerWrapper, the drawer panel body, for imperative needs such as drag resize |
placement | 'left' | 'right' | 'top' | 'bottom' | "right" | Position of the drawer |
open | boolean | - | Controls whether the drawer is open or closed |
onOpenChange | (open: boolean) => void | - | Callback when the drawer state changes. Required |
onBackdropClick | (e: React.MouseEvent<HTMLDialogElement>) => void | - | Click on the dialog backdrop; close by calling onOpenChange(false) |
onCancel | (e: React.SyntheticEvent<HTMLDialogElement>) => void | - | Extra callback when the dialog is cancelled (Esc); the default close is always applied first |
drag | DrawerDragApi | - | Drag gesture object used to implement a resizable drawer |
className | ClassNameValue | - | Custom classes, applied to the native <dialog> element |
style | React.CSSProperties | - | Inline styles, applied to the native <dialog> element |
classNames | { wrapper?: ClassNameValue; drag?: ClassNameValue; content?: ClassNameValue } | - | Custom classes for the internal elements |
styles | { wrapper?: React.CSSProperties; drag?: React.CSSProperties; content?: React.CSSProperties } | - | Inline styles for the internal elements |
children | React.ReactNode | - | Drawer panel content |
...props | React.ComponentProps<"div"> | - | Forwarded to DrawerWrapper; supports all native div props |
Composable Components
DrawerRoot
The dialog element, rendered as a plain native dialog with no built-in styles or behavior.
| Prop | Type | Default | Description |
|---|---|---|---|
className | ClassNameValue | - | Custom class name, applied to the native dialog element |
...props | React.ComponentProps<"dialog"> | - | Supports all native dialog props (except className) |
DrawerWrapper
The sliding panel body with built-in positioning, flex layout, shadow and slide transition. Size defaults to a minimum of min-w-1/4 when horizontal and min-h-1/3 when vertical, full size on the other axis; override or grow via className/style.
| Prop | Type | Default | Description |
|---|---|---|---|
placement | 'left' | 'right' | 'top' | 'bottom' | - | Position of the drawer, drives placement styles |
isHorizontal | boolean | - | Whether the drawer slides from left/right |
className | ClassNameValue | - | Custom class name |
...props | React.ComponentProps<"div"> | - | Supports all native div props (except className) |
DrawerDrag
Drag handle that renders a round indicator bar. The drag logic is provided by the external useDrag hook and bound via onPointerDown.
| Prop | Type | Default | Description |
|---|---|---|---|
isHorizontal | boolean | - | Whether horizontal, determining the indicator orientation |
className | ClassNameValue | - | Custom class name |
...props | React.ComponentProps<"div"> | - | Supports all native div props (except className) |
DrawerContent
Content panel container with built-in padding, background and scrolling.
| Prop | Type | Default | Description |
|---|---|---|---|
isHorizontal | boolean | - | Whether horizontal, affects width and orientation; content is centered when vertical |
className | ClassNameValue | - | Custom class name |
children | React.ReactNode | - | Panel content |
...props | React.ComponentProps<"div"> | - | Supports all native div props (except className) |