Drawer
Headless draggable panel primitive built on Dialog with snap points and momentum physics.
Drawer
A headless draggable panel primitive. Built on the Dialog primitive with pointer-based drag physics, snap point management, and directional support. Apply your own styles — no design tokens are included.
For a fully styled drawer with design system tokens, use @loke/design-system/drawer instead.
Usage
import {
Root,
Trigger,
Portal,
Overlay,
Content,
Handle,
Title,
Description,
Close,
} from '@loke/ui/drawer';
export default function Example() {
return (
<Root>
<Trigger asChild>
<button>Open</button>
</Trigger>
<Portal>
<Overlay />
<Content>
<Handle />
<Title>Drawer Title</Title>
<Description>Description text.</Description>
<Close asChild>
<button>Close</button>
</Close>
</Content>
</Portal>
</Root>
);
}Sub-components
- Root /
Drawer— Root provider; manages open state, drag physics, and snap points - Trigger /
DrawerTrigger— Element that opens the drawer - Portal /
DrawerPortal— Renders content outside the DOM tree (via Dialog portal) - Overlay /
DrawerOverlay— Backdrop element; receives drag pointer events - Content /
DrawerContent— Drawer panel; applies translate transform during drag - Handle /
DrawerHandle— Drag grip with keyboard support for snap/close - Title /
DrawerTitle— Accessible title (required for screen readers) - Description /
DrawerDescription— Accessible description - Close /
DrawerClose— Element that closes the drawer - NestedRoot /
DrawerNestedRoot— Shorthand for<Root nested>for drawers inside drawers
Props
Root
Prop
Type
Content
Prop
Type
Data attributes
Content and Overlay receive these attributes for custom styling:
| Attribute | Values | Description |
|---|---|---|
data-direction | "bottom" | "top" | "left" | "right" | Current drawer direction |
data-dragging | "true" | undefined | Present while a drag gesture is active |
data-loke-drawer-direction | same as data-direction | Mirrored on Content for CSS selector targeting |
Use [data-loke-drawer-direction=bottom] selectors to style direction-specific variants:
[data-loke-drawer-direction="bottom"] {
border-radius: 12px 12px 0 0;
}
[data-loke-drawer-direction="right"] {
border-radius: 12px 0 0 12px;
}Background scaling
To scale the page background when a drawer opens, wrap your app content in a [data-loke-drawer-wrapper] element and set shouldScaleBackground on Root:
// layout.tsx
<div data-loke-drawer-wrapper>
{children}
</div>
// usage
<Root shouldScaleBackground>
...
</Root>Examples
Basic drawer
Controlled state
Drawer is: closed
Accessibility
- Escape closes the drawer.
- Tab cycles focus within the drawer (focus trap via Dialog).
- Handle has
role="separator"andtabIndex={0}. - Enter / Space on the handle advances to the next snap point or closes.
- ↑ / → on the handle moves to a higher snap point.
- ↓ / ← on the handle moves to a lower snap point.
- Focus returns to the trigger on close.
Best practices
- Always provide a
Title— even visually hidden — for screen reader context. - Use
NestedRootwhen mounting a drawer inside another drawer to prevent drag event conflicts. - Prefer fractional snap points (0–1) over pixel values for responsive layouts.
- Set
handleOnlywhen the drawer contains scrollable content that shouldn't initiate a drag.