Skip to Content

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>
  • left and right drawers have a fixed width of w-80.
  • bottom drawer spans the full width with automatic height (up to 80dvh) 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 initialFocus a ref to a top element, such as the Close button or the Title with tabIndex={-1}, to open at the top.
  • With nothing tabbable inside, or with initialFocus on the popup itself, the popup takes focus. When the browser shows focus there (after keyboard use, for example), the ring is 2px in --ring and 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} or modal="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 (with alignItemWithTrigger={false} on Select.Positioner, or opened by touch): cap them at --available-height and let them scroll. In a Popover, scroll an element inside Popover.Content, as overflow on the popup clips Popover.Arrow.
  • A position class in the Portal’s className, such as absolute, replaces fixed, and the popups move away from their triggers again when the page behind scrolls or changes height.

Compound Parts

PartDescription
Drawer.RootState container. Manages open/close state.
Drawer.TriggerButton that opens the drawer.
Drawer.PortalRenders children in a portal. Puts the drawer on the z-drawers (600) layer; popups, dialogs and drawers opened from it stack above it.
Drawer.BackdropSemi-transparent overlay. Also renders for a nested drawer, so it dims the parent.
Drawer.PopupThe sliding panel. Accepts side prop.
Drawer.TitleDrawer heading.
Drawer.DescriptionSupporting description text.
Drawer.BodyOptional scrolling area for long content, between the Title and the actions. Only the Body scrolls; the other parts stay in place.
Drawer.CloseButton 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

NameTypeDefault
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.

Last updated on