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.
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(defaultfalse) — 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) (defaulttrue) - disablePointerDismissal —
boolean(defaultfalse) — keep the drawer open on outside press - swipeDirection —
up·down·left·right(defaultdown) — 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.Popuprenders as a dialog:Drawer.Titlelabels it througharia-labelledbyandDrawer.Descriptiondescribes it througharia-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
disablePointerDismissalto 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
Contentso they’re reachable without scrolling. - Don’t stack drawers; open one at a time so the swipe and focus behavior stays predictable.