Drawer
A side or bottom sheet overlay built on Base UI’s Dialog primitive. Slides in from the left, right, or bottom edge.
Import
import { Drawer } from "chunks-ui";Basic Usage
<Drawer.Root>
<Drawer.Trigger>Open Drawer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Popup>
<Drawer.Title>Navigation</Drawer.Title>
<Drawer.Description>Browse the app sections.</Drawer.Description>
<nav className="mt-4 flex flex-col gap-2">
<a href="/">Home</a>
<a href="/theme">Theme</a>
</nav>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>Side Variants
The side prop on Drawer.Popup controls which edge the drawer slides from:
{/* Right side (default) */}
<Drawer.Popup side="right">...</Drawer.Popup>
{/* Left side */}
<Drawer.Popup side="left">...</Drawer.Popup>
{/* Bottom sheet */}
<Drawer.Popup side="bottom">...</Drawer.Popup>leftandrightdrawers have a fixed width ofw-80.bottomdrawer spans the full width with automatic height (up to80dvh) and rounded top corners.- Content taller than the drawer scrolls inside it. To keep the Title and the actions in place, put the content in a
Drawer.Body(below). - With long content and actions only at the bottom, Base UI focuses the first tabbable element and scrolls to it. Pass
initialFocusa ref to a top element, such as the Close button or the Title withtabIndex={-1}, to open at the top. - With nothing tabbable inside, or with
initialFocuson the popup itself, the popup takes focus. When the browser shows focus there (after keyboard use, for example), the ring is 2px in--ringand drawn just inside the 1px border on the open side, 3px from the popup’s edge, so the border stays visible.
Long content
Put long content in a Drawer.Body between the Title and the actions. The Title, the Description and the actions stay in place, and only the Body scrolls. In a left or right drawer the Body fills the height, so the actions sit at the bottom edge; a bottom drawer grows with its content up to 80dvh:
<Drawer.Root>
<Drawer.Trigger>Read the terms</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Popup>
<Drawer.Title>Terms of service</Drawer.Title>
<Drawer.Description>Read the terms before you accept them.</Drawer.Description>
<Drawer.Body className="mt-4 space-y-3 text-sm">
<p>These terms apply to your use of the service.</p>
<p>Keep your account details safe.</p>
</Drawer.Body>
<div className="mt-4 flex justify-end gap-2">
<Drawer.Close render={<Button variant="outlined" color="secondary" />}>
Cancel
</Drawer.Close>
<Drawer.Close render={<Button />}>Accept</Drawer.Close>
</div>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>Drawer.Body works as Dialog.Body: the tab stop, data-overflowing, the name from the Title and the form through render. See Dialog.
Controlled
function ControlledDrawer() {
const [open, setOpen] = useState(false);
return (
<Drawer.Root open={open} onOpenChange={setOpen}>
<Drawer.Trigger>Menu</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Popup side="left">
<Drawer.Title>Menu</Drawer.Title>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>
);
}Animation
The drawer slides in from its edge: on the x-axis for left and right, on the y-axis for bottom. With Motion installed it uses the overlay spring (springs.overlay). Without Motion it is a CSS transition (micro-interactions: 300ms, cubic-bezier(0.4, 0, 0.2, 1)). Reduced motion turns off both. The backdrop fades in independently.
render on Drawer.Popup or Drawer.Backdrop replaces the element the spring animates, so a popup or backdrop with render uses the CSS transition even with Motion installed. To restyle either, pass className; to change the element of the content, put render on Drawer.Body.
Popups in a drawer
As in a Dialog, a popup opened from the drawer is placed against Drawer.Portal, a box fixed to the top of the viewport, so it stays at its trigger when the page behind scrolls or changes height. That has two limits:
- A popup taller than the viewport is clipped at the viewport’s edge: a modal drawer locks the page’s scroll, and in a
modal={false}ormodal="trap-focus"drawer the page scrolls but the fixed box adds no height to it. Menu and Combobox cap their height at the space available (--available-height) and scroll. A Popover does not, nor a Select that is not aligned over its trigger (withalignItemWithTrigger={false}onSelect.Positioner, or opened by touch): cap them at--available-heightand let them scroll. In a Popover, scroll an element insidePopover.Content, asoverflowon the popup clipsPopover.Arrow. - A position class in the Portal’s
className, such asabsolute, replacesfixed, and the popups move away from their triggers again when the page behind scrolls or changes height.
Compound Parts
| Part | Description |
|---|---|
Drawer.Root | State container. Manages open/close state. |
Drawer.Trigger | Button that opens the drawer. |
Drawer.Portal | Renders children in a portal. Puts the drawer on the z-drawers (600) layer; popups, dialogs and drawers opened from it stack above it. |
Drawer.Backdrop | Semi-transparent overlay. Also renders for a nested drawer, so it dims the parent. |
Drawer.Popup | The sliding panel. Accepts side prop. |
Drawer.Title | Drawer heading. |
Drawer.Description | Supporting description text. |
Drawer.Body | Optional scrolling area for long content, between the Title and the actions. Only the Body scrolls; the other parts stay in place. |
Drawer.Close | Button that closes the drawer. |
Drawer.Root Props
All Drawer.Root props are forwarded to the underlying Base UI Dialog.
Drawer.Body Props
Drawer.Body takes the props of Dialog.Body: a <section>’s, plus Base UI’s render.
Drawer.Popup Props
| Name | Type | Default |
|---|---|---|
side | "left" | "right" | "bottom"Which edge of the viewport the drawer slides in from. | "right" |
All other props are forwarded to the underlying Base UI Dialog.Popup.