Photon Design System

Drawer

A panel that slides in from the edge — here, the right — with swipe-to-dismiss. Built on Base UI Drawer. Use it for secondary tasks and navigation that should keep the page context behind it.

Import

import { Drawer } from "@photon-ai/pho-ui/components/drawer";

Basic

Compose Drawer.Portal → Backdrop → Viewport → Popup → Content. Wrap action buttons in Drawer.Close to dismiss.

<Drawer.Root>
  <Drawer.Trigger render={<Button variant="outlined">Open settings</Button>} />
  <Drawer.Portal>
    <Drawer.Backdrop />
    <Drawer.Viewport>
      <Drawer.Popup>
        <Drawer.Content>
          <Drawer.Title>Project settings</Drawer.Title>
          <Drawer.Description>
            Update how this project deploys.
          </Drawer.Description>
          …
          <Drawer.Close render={<Button>Save changes</Button>} />
        </Drawer.Content>
      </Drawer.Popup>
    </Drawer.Viewport>
  </Drawer.Portal>
</Drawer.Root>

Controlled

Leave the drawer uncontrolled and let Drawer.Trigger own the open state, or drive it yourself: pass open and onOpenChange to Drawer.Root to open it from anywhere — a menu item, a keyboard shortcut, a completed request. A controlled drawer needs no Drawer.Trigger; when it closes, focus returns to whatever was focused before it opened.

Panel is closed
const [open, setOpen] = useState(false);

<Button variant="outlined" onClick={() => setOpen(true)}>
  Open panel
</Button>
<Drawer.Root open={open} onOpenChange={setOpen}>
  <Drawer.Portal>…</Drawer.Portal>
</Drawer.Root>

Anatomy

  • Drawer.Root — groups the parts and owns the open state; renders no element
  • Drawer.Trigger — the <button> that opens the drawer
  • Drawer.Portal — renders the overlay layers into a portal at the document root
  • Drawer.Backdrop — the dimmed, blurred scrim behind the panel
  • Drawer.Viewport — full-screen flex layer that pins the panel to the right edge
  • Drawer.Popup — the sliding panel itself (the dialog), with the swipe transform
  • Drawer.Content — flex column for the panel body
  • Drawer.Title — <h2> that labels the dialog
  • Drawer.Description — <p> that describes the dialog
  • Drawer.Close — a <button> that dismisses the drawer

Props

Drawer.Root owns the behavior:

  • open — boolean — controlled open state
  • defaultOpen — boolean (default false) — uncontrolled initial state
  • onOpenChange — (open: boolean, event) => void, fired on every open/close
  • modal — true (trap focus, lock page scroll, block outside pointers) · 'trap-focus' (trap focus only) · false (leave the page interactive) (default true)
  • disablePointerDismissal — boolean (default false) — keep the drawer open on outside press
  • swipeDirection — up · down · left · right (default down) — gesture direction that dismisses the drawer

Drawer.Popup refines focus:

  • initialFocus — element focused when the drawer opens (defaults to the panel)
  • finalFocus — element focused when it closes (defaults to the trigger)

Every styled part (Backdrop, Viewport, Popup, Content, Title, Description) takes className to extend its styling; all parts accept Base UI’s render prop for swapping the underlying element. Advanced Drawer.Root options — imperative actionsRef, detached handle, and bottom-sheet snapPoints — pass straight through to Base UI.

Accessibility

  • Drawer.Popup renders as a dialog: Drawer.Title labels it through aria-labelledby and Drawer.Description describes it through aria-describedby, both wired automatically when the parts are present.
  • With the default modal, focus is trapped inside the panel, page scroll is locked, and pointer interaction with the rest of the document is disabled.
  • Focus: opening moves focus into the panel; closing returns it to the trigger. Override either end with the Popup’s initialFocus / finalFocus.
  • Outside press closes the drawer by default; set disablePointerDismissal to require an explicit close.

Keyboard:

  • Escape — close the drawer
  • Tab / Shift+Tab — cycle focus within the panel while it is modal
  • Enter / Space — activate Drawer.Trigger / Drawer.Close (real <button>s)

Best practices

  • Use a drawer for side tasks (settings, filters, a detail panel) where keeping the page visible behind it helps. For a decision, or any centered modal, use Dialog.
  • Keep primary actions pinned at the bottom of the Content so they’re reachable without scrolling.
  • Don’t stack drawers; open one at a time so the swipe and focus behavior stays predictable.