Drawer
A high-quality, unstyled React drawer component with swipe-to-dismiss gestures.
A panel that slides in from the edge of the screen.
Note: Drawer is in preview for a temporary period and receives breaking changes in minor versions. DrawerPreview will be renamed to Drawer when it exits preview.
Usage guidelines
- Drawer extends Dialog: It adds gesture support, snap points, and indent effects. If you don't need these, use Dialog instead. A panel that slides in from the edge of the screen and doesn't need gesture support is a positioned Dialog.
Anatomy
Import the component and assemble its parts:
import { DrawerPreview as Drawer } from '@base-ui/react/drawer';
<Drawer.Provider>
<Drawer.IndentBackground />
<Drawer.Indent>
<Drawer.Root>
<Drawer.Trigger />
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Viewport>
<Drawer.Popup>
<Drawer.Content>
<Drawer.Title />
<Drawer.Description />
<Drawer.Close />
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>
</Drawer.Indent>
</Drawer.Provider>;Drawer supports swipe gestures to dismiss. Set swipeDirection to control which direction dismisses the drawer. <Drawer.Content> allows text selection of its children without swipe interference when using a mouse pointer.
Examples
State
By default, Drawer is an uncontrolled component that manages its own state.
<Drawer.Root>
<Drawer.Trigger>Open</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Viewport>
<Drawer.Popup>
<Drawer.Content>
<Drawer.Title>Example drawer</Drawer.Title>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>Use open and onOpenChange props if you need to access or control the state of the drawer.
const [open, setOpen] = React.useState(false);
return (
<Drawer.Root open={open} onOpenChange={setOpen}>
<Drawer.Trigger>Open</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Viewport>
<Drawer.Popup>
<Drawer.Content>
<Drawer.Title>Example drawer</Drawer.Title>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>
);Position
Positioning is handled by your styles. swipeDirection defaults to "down" for bottom sheets. Use "up", "left", or "right" for other drawer positions.
<Drawer.Root swipeDirection="right">Nested drawers
Use the [data-nested-drawer-open] selector and the --nested-drawers CSS variable to style drawers when a nested drawer is open.
This demo stacks nested drawers using a constant peek so the frontmost drawer stays anchored to the bottom while the ones behind it are scaled down and lifted. It also uses the --drawer-height and --drawer-frontmost-height CSS variables to handle varying drawer heights.
Snap points
Use snapPoints to snap a bottom sheet drawer to preset heights. Numbers between 0 and 1 represent fractions of the viewport height, and numbers greater than 1 are treated as pixel values. String values support px and rem units (for example, '148px' or '30rem').
const snapPoints = ['148px', 1];
const [snapPoint, setSnapPoint] = React.useState<Drawer.Root.SnapPoint | null>(snapPoints[0]);
<Drawer.Root snapPoints={snapPoints} snapPoint={snapPoint} onSnapPointChange={setSnapPoint}>
{/* ... */}
</Drawer.Root>;Apply the snap point offset in your styles when using vertical drawers:
.DrawerPopup {
transform: translateY(calc(var(--drawer-snap-point-offset) + var(--drawer-swipe-movement-y)));
}By default, the drawer can skip snap points when swiping quickly. Specify the snapToSequentialPoints prop to disable velocity-based skipping so the snap target is determined by drag distance (you can still drag past multiple points).
Indent effect
Scale the background down when any drawer opens by wrapping your app in <Drawer.Provider> and use <Drawer.IndentBackground> + <Drawer.Indent> at the top of your tree. Any <Drawer.Root> within the provider notifies it when it mounts, which activates the indent parts (they receive [data-active] state attributes).
Non-modal
Set modal={false} to opt out of focus trapping and disablePointerDismissal to keep the drawer open on outside clicks.
Mobile navigation
You can build a full-screen mobile navigation sheet using Drawer parts, including a flick-to-dismiss from the top gesture.
Action sheet with separate destructive action
This demo builds an action sheet with a grouped list of actions plus a separate destructive action button.
Detached triggers
A drawer can be controlled by a trigger located either inside or outside the <Drawer.Root> component. For simple, one-off interactions, place the <Drawer.Trigger> inside <Drawer.Root>.
However, if defining the drawer's content next to its trigger is not practical, you can use a detached trigger. This involves placing the <Drawer.Trigger> outside of <Drawer.Root> and linking them with a handle created by the Drawer.createHandle() function.
const demoDrawer = Drawer.createHandle();
<Drawer.Trigger handle={demoDrawer}>Open</Drawer.Trigger>
<Drawer.Root handle={demoDrawer}>
...
</Drawer.Root>The drawer can render different content depending on which trigger opened it. This is achieved by passing a payload to the <Drawer.Trigger> and using the function-as-a-child pattern in <Drawer.Root>.
const demoDrawer = Drawer.createHandle<{ title: string }>();
<Drawer.Trigger handle={demoDrawer} payload={{ title: 'Profile' }}>
Profile
</Drawer.Trigger>
<Drawer.Trigger handle={demoDrawer} payload={{ title: 'Settings' }}>
Settings
</Drawer.Trigger>
<Drawer.Root handle={demoDrawer}>
{({ payload }) => (
<Drawer.Portal>
<Drawer.Popup>
<Drawer.Content>
<Drawer.Title>{payload?.title}</Drawer.Title>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Portal>
)}
</Drawer.Root>Stacking and animations
Use CSS transitions or animations to animate drawer opening, closing, swipe interactions, and nested stacking. The data-starting-style attribute is applied when a drawer starts to open, and data-ending-style is applied when it starts to close.
The --nested-drawers CSS variable can be used to determine stack depth. The frontmost drawer has index 0.
.DrawerPopup {
--stack-step: 0.05;
--stack-scale: calc(1 - (var(--nested-drawers) * var(--stack-step)));
transform: translateY(var(--drawer-swipe-movement-y)) scale(var(--stack-scale));
}When stacked drawers have varying heights, use the --drawer-height and --drawer-frontmost-height variables to keep collapsed drawers aligned with the frontmost one.
.DrawerPopup {
--bleed: 3rem;
--stack-height: max(
0px,
calc(var(--drawer-frontmost-height, var(--drawer-height)) - var(--bleed))
);
height: var(--drawer-height, auto);
}
.DrawerPopup[data-nested-drawer-open] {
height: calc(var(--stack-height) + var(--bleed));
overflow: hidden;
}The data-nested-drawer-open attribute marks drawers behind the frontmost drawer. Use it with data-nested-drawer-swiping to dim or hide parent drawer content while keeping it visible during nested swipe interactions.
.DrawerContent {
transition: opacity 300ms;
}
.DrawerPopup[data-nested-drawer-open] .DrawerContent {
opacity: 0;
}
.DrawerPopup[data-nested-drawer-open][data-nested-drawer-swiping] .DrawerContent {
opacity: 1;
}The --drawer-swipe-movement-x, --drawer-swipe-movement-y, and --drawer-snap-point-offset CSS variables can be used to create smooth drag and snap offsets:
.DrawerPopup[data-swipe-direction='right'] {
transform: translateX(var(--drawer-swipe-movement-x));
}
.DrawerPopup[data-swipe-direction='down'] {
transform: translateY(calc(var(--drawer-snap-point-offset) + var(--drawer-swipe-movement-y)));
}The data-swipe-direction attribute can be used with data-ending-style to animate directional dismissal:
.DrawerPopup[data-ending-style][data-swipe-direction='right'] {
transform: translateX(100%);
}
.DrawerPopup[data-ending-style][data-swipe-direction='down'] {
transform: translateY(100%);
}Use --drawer-swipe-progress to fade the backdrop as the drawer is swiped, and --drawer-swipe-strength to scale release transition durations based on swipe velocity.
.DrawerBackdrop {
--backdrop-opacity: 0.2;
opacity: calc(var(--backdrop-opacity) * (1 - var(--drawer-swipe-progress)));
}
.DrawerPopup[data-ending-style],
.DrawerBackdrop[data-ending-style] {
transition-duration: calc(var(--drawer-swipe-strength) * 400ms);
}
.DrawerPopup[data-swiping],
.DrawerBackdrop[data-swiping] {
transition-duration: 0ms;
}API reference
Provider
| Name | Type | Default | Description |
|---|---|---|---|
childrenOptional | React.ReactNode | — | — |
Root
| Name | Type | Default | Description |
|---|---|---|---|
openOptional | boolean | undefined | — | Whether the drawer is currently open. |
defaultOpenOptional | boolean | undefined | false | Whether the drawer is initially open. To render a controlled drawer, use the `open` prop instead. |
modalOptional | boolean | 'trap-focus' | undefined | true | Determines if the drawer enters a modal state when open. - `true`: user interaction is limited to just the drawer: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the drawer, but document page scroll is not locked and pointer interactions outside of it remain enabled. |
onOpenChangeOptional | ((open: boolean, eventDetails: DrawerRoot.ChangeEventDetails) => void) | undefined | — | Event handler called when the drawer is opened or closed. |
onOpenChangeCompleteOptional | ((open: boolean) => void) | undefined | — | Event handler called after any animations complete when the drawer is opened or closed. |
disablePointerDismissalOptional | boolean | undefined | false | Determines whether the drawer should close on outside clicks. |
actionsRefOptional | React.RefObject<DrawerRoot.Actions | null> | undefined | — | A ref to imperative actions. - `unmount`: When specified, the drawer will not be unmounted when closed. Instead, the `unmount` function must be called to unmount the drawer manually. Useful when the drawer's animation is controlled by an external library. - `close`: Closes the drawer imperatively when called. |
handleOptional | DialogHandle<Payload> | undefined | — | A handle to associate the drawer with a trigger. If specified, allows detached triggers to control the drawer's open state. Can be created with the Drawer.createHandle() method. |
triggerIdOptional | string | null | undefined | — | ID of the trigger that the drawer is associated with. This is useful in conjunction with the `open` prop to create a controlled drawer. There's no need to specify this prop when the drawer is uncontrolled (i.e. when the `open` prop is not set). |
defaultTriggerIdOptional | string | null | undefined | — | ID of the trigger that the drawer is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open drawer. |
childrenOptional | React.ReactNode | PayloadChildRenderFunction<Payload> | — | The content of the drawer. |
swipeDirectionOptional | DrawerSwipeDirection | undefined | down | The swipe direction used to dismiss the drawer. |
snapPointsOptional | DrawerSnapPoint[] | undefined | — | Snap points used to position the drawer. Use numbers between 0 and 1 to represent fractions of the viewport height, numbers greater than 1 as pixel values, or strings in `px`/`rem` units (for example, `'148px'` or `'30rem'`). |
snapToSequentialPointsOptional | boolean | undefined | false | Disables velocity-based snap skipping so drag distance determines the next snap point. |
snapPointOptional | DrawerSnapPoint | null | undefined | — | The currently active snap point. Use with `onSnapPointChange` to control the snap point. |
defaultSnapPointOptional | DrawerSnapPoint | null | undefined | — | The initial snap point value when uncontrolled. |
snapPointRequired | DrawerSnapPoint | null, | — | — |
eventDetailsRequired | DrawerRoot.SnapPointChangeEventDetails, | — | — |
Trigger
| Name | Type | Default | Description |
|---|---|---|---|
handleOptional | DrawerHandle<Payload> | undefined | — | A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method. |
payloadOptional | Payload | undefined | — | A payload to pass to the drawer when it is opened. |
idOptional | string | undefined | — | ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for drawers in controlled mode (with the Drawer.Root `triggerId` prop). |
Portal
| Name | Type | Default | Description |
|---|---|---|---|
keepMountedOptional | boolean | undefined | false | Whether to keep the portal mounted in the DOM while the popup is hidden. |
containerOptional | FloatingPortal.Props<DrawerPortalState>['container'] | undefined | — | A parent element to render the portal element into. |
Backdrop
| Name | Type | Default | Description |
|---|---|---|---|
forceRenderOptional | boolean | undefined | false | Whether the backdrop is forced to render even when nested. |