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:

AttributeValuesDescription
data-direction"bottom" | "top" | "left" | "right"Current drawer direction
data-dragging"true" | undefinedPresent while a drag gesture is active
data-loke-drawer-directionsame as data-directionMirrored 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" and tabIndex={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 NestedRoot when mounting a drawer inside another drawer to prevent drag event conflicts.
  • Prefer fractional snap points (0–1) over pixel values for responsive layouts.
  • Set handleOnly when the drawer contains scrollable content that shouldn't initiate a drag.