Drawer
A panel that slides in from the edge of the screen.
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:
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.
Add data-base-ui-swipe-ignore to a descendant to opt it out of swipe dismissal for all input types. If the element only handles touch drags along one axis, such as a JavaScript carousel, set the value to x or y so touch drags along the other axis still swipe the drawer.
Use <Drawer.VirtualKeyboardProvider> when a bottom sheet contains form fields and you want Base UI to manage keyboard-aware focus and scroll handling for software keyboards. Drawers without this provider are unaffected.
Examples
State
By default, Drawer is an uncontrolled component that manages its own state.
Use bind:open and the onOpenChange callback if you need to access or control the state of the drawer. Without bind:, the drawer manages its own state.
Position
Positioning is handled by your styles. swipeDirection defaults to "down" for bottom sheets. Use "up", "left", or "right" for other drawer positions.
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').
Apply the snap point offset in your styles when using vertical drawers:
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).
Virtual keyboard aware
Wrap a bottom sheet in <Drawer.VirtualKeyboardProvider> to make it react to software keyboards when it contains form controls. When the keyboard opens, the provider scrolls the body to keep the focused field visible.
- Keep the popup frame stable: place header and footer content outside a plain scrollable body.
- Lift a pinned footer input: if a footer contains its own input, reserve a footer slot below the scroll area and offset it by
var(--drawer-keyboard-inset, 0px). The demo switches the focused footer toposition: fixed— positioned against the popup, since itstransformcontains fixed descendants — and adds the inset to its bottom padding. - Always include the
0pxfallback: the provider only sets--drawer-keyboard-insetwhile the keyboard is aligned, so a barevar(--drawer-keyboard-inset)is invalid before the first alignment and after cleanup.
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.
Swipe to open
Place <Drawer.SwipeArea> along the edge of the viewport to enable swipe-to-open gestures.
Swipe from the right edge to open the drawer.
Close confirmation
This example shows a nested confirmation dialog that opens if the text entered in the drawer is going to be discarded.
To implement this, both the drawer and the confirmation dialog should be controlled. The confirmation dialog may be opened when the onOpenChange callback of the drawer receives a request to close while there is text in the textarea. This way, the confirmation is automatically shown when the user clicks the backdrop, presses the Esc key, clicks a close button, or dismisses the drawer with a swipe gesture.
Use eventDetails.cancel() in onOpenChange to prevent the drawer from closing while the confirmation prompt is shown.
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.
The imperative methods on the handle, such as open() and openWithPayload(), require a <Drawer.Root> using the same handle to be mounted.
Calls made while no root is attached to the handle — before one mounts, or after it unmounts — are ignored. Each time a root mounts, it starts from fresh state: a call made while no root was attached is not replayed, and no open state carries over from a previous mount.
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 snippet-as-a-child pattern in <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.
When stacked drawers have varying heights, use the --drawer-height and --drawer-frontmost-height variables to keep collapsed drawers aligned with the frontmost one.
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.
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:
The data-swipe-direction attribute can be used with data-ending-style to animate directional dismissal:
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.
API reference
Provider
Coordinates page-level drawer UI, such as Drawer.Indent, across the drawers inside it. Doesn't render its own HTML element.
This part takes no props.
IndentBackground
A background layer placed before Drawer.Indent. Renders a <div> element. data-active is present while any drawer inside the nearest Drawer.Provider is open.
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerIndentBackgroundState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerIndentBackgroundState, children: Snippet | undefined]>
active boolean
- Name
active- Description
Whether any drawer in the nearest
Drawer.Provideris open.- Type
boolean
data-active
data-inactive
Indent
A wrapper for the app's main UI. Renders a <div> element. data-active is present while any drawer inside the nearest Drawer.Provider is open. The frontmost drawer's swipe progress and height reach it as CSS variables.
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerIndentState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerIndentState, children: Snippet | undefined]>
active boolean
- Name
active- Description
Whether any drawer in the nearest
Drawer.Provideris open.- Type
boolean
data-active
data-inactive
--drawer-swipe-progress
0 to 1.--drawer-height
Root
Groups all parts of the drawer. Doesn't render its own HTML element.
onOpenChange function —
- Name
- Description
Called before the open state changes. Call
eventDetails.cancel()to veto it.- Type
(open: boolean, eventDetails: DrawerChangeEventDetails) => void
handle DrawerHandle<Payload> —
- Name
- Description
Connects detached triggers. Create one with
Drawer.createHandle().- Type
DrawerHandle<Payload>
swipeDirection DrawerSwipeDirection 'down'
- Name
- Description
The direction a swipe dismisses the drawer in.
- Type
DrawerSwipeDirection- Default
'down'
snapPoints DrawerSnapPoint[] —
- Name
- Description
Snap points that size the drawer. Numbers from 0 to 1 are fractions of the viewport height. Larger numbers are pixels. Strings take
pxorremunits, such as'148px'or'30rem'.- Type
DrawerSnapPoint[]
snapToSequentialPoints boolean false
- Description
Let the drag distance pick the next snap point, without skipping on velocity.
- Type
boolean- Default
false
snapPoint DrawerSnapPoint | null —
- Name
- Description
The active snap point. Use
bind:snapPointto share it with the parent. A value outsidesnapPointsresolves to the closest snap point.- Type
DrawerSnapPoint | null- Binding
bind:snapPoint
defaultSnapPoint DrawerSnapPoint | null —
- Name
- Description
The snap point the drawer starts at when
snapPointis unset and returns to when it closes. Defaults to the first ofsnapPoints.- Type
DrawerSnapPoint | null
onSnapPointChange function —
- Name
- Description
Called before the snap point changes. Call
eventDetails.cancel()to veto it.- Type
(snapPoint: DrawerSnapPoint | null, eventDetails: DrawerSnapPointChangeEventDetails) => void
children Snippet<[{ payload: Payload | undefined }]> —
- Name
- Type
Snippet<[{ payload: Payload | undefined }]>
modal boolean | 'trap-focus' true
- Name
- Description
truetraps focus, locks page scroll, and blocks outside pointer interaction.falseleaves the rest of the page usable.'trap-focus'traps focus without locking scroll or outside pointers.- Type
boolean | 'trap-focus'- Default
true
disablePointerDismissal boolean false
- Description
Do not close on outside presses. Non-modal dialogs also stay open when focus moves outside.
- Type
boolean- Default
false
open boolean false
- Name
- Description
Whether the dialog is open. Use
bind:opento share it with the parent. A value passed withoutbind:holds until the parent changes it.- Type
boolean- Default
false- Binding
bind:open
onOpenChangeComplete function —
- Name
- Description
Called after the open or close animation finishes.
- Type
(open: boolean) => void
triggerId string | null null
- Name
- Description
Id of the trigger that owns the dialog. Use
bind:triggerIdto share it with the parent. Pass it with an initialopento name the trigger the dialog starts from. An id written by a trigger press resets tonullonce the popup unmounts.- Type
string | null- Default
null- Binding
bind:triggerId
Trigger
A button that opens the drawer. Renders a <button> element.
handle DrawerHandle<Payload> —
- Name
- Description
Associates this trigger with a drawer that is not its parent.
- Type
DrawerHandle<Payload>
id string | null —
- Name
- Type
string | null
payload Payload —
- Name
- Description
Passed to the dialog when this trigger opens it.
- Type
Payload
nativeButton boolean true
- Name
- Description
falserenders throughrenderinstead of a native button.- Type
boolean- Default
true
render Snippet<[props: DialogTriggerHostProps, state: DialogTriggerState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: DialogTriggerHostProps, state: DialogTriggerState, children: Snippet | undefined]>
disabled boolean
- Name
disabled- Description
Whether the trigger is currently disabled.
- Type
boolean
open boolean
- Name
open- Description
Whether the dialog is open and was opened by this trigger.
- Type
boolean
data-popup-open
data-disabled
SwipeArea
An invisible area that opens the drawer with a swipe. Renders a <div> element.
disabled boolean | null false
- Name
- Description
Ignore swipes.
- Type
boolean | null- Default
false
swipeDirection DrawerSwipeDirection —
- Name
- Description
The direction a swipe opens the drawer in. Defaults to the opposite of the root's
swipeDirection.- Type
DrawerSwipeDirection
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerSwipeAreaState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerSwipeAreaState, children: Snippet | undefined]>
open boolean
- Name
open- Description
Whether the drawer is open.
- Type
boolean
swiping boolean
- Name
swiping- Description
Whether the swipe area is being swiped.
- Type
boolean
swipeDirection DrawerSwipeDirection
- Name
swipeDirection- Description
The direction a swipe opens the drawer in.
- Type
DrawerSwipeDirection
disabled boolean
- Name
disabled- Description
Whether the swipe area is disabled.
- Type
boolean
data-open
data-closed
data-disabled
data-swipe-direction
data-swiping
VirtualKeyboardProvider
Keyboard-aware focus and scroll handling for bottom-sheet drawers with form fields. Doesn't render its own HTML element. It needs a Drawer.Viewport, which receives the --drawer-keyboard-inset variable.
This part takes no props.
Portal
A portal element that moves the popup to a different part of the DOM. Renders through the shared FloatingPortal, which appends to <body> by default.
keepMounted boolean false
- Name
- Description
Keep the portal mounted while the popup is closed.
- Type
boolean- Default
false
container HTMLElement | ShadowRoot | null —
- Name
- Description
Element the portal is appended to.
undefineduses the parent portal ordocument.body.nullwaits and does not mount until an element or shadow root is set. There is no ref object.- Type
HTMLElement | ShadowRoot | null
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DialogPortalState, children: Snippet | undefined]> —
- Name
- Description
Replace the portal element. The snippet receives host props, an empty state, and the children snippet.
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DialogPortalState, children: Snippet | undefined]>
Backdrop
An overlay displayed beneath the popup. Renders a <div> element.
forceRender boolean false
- Name
- Description
Render the backdrop even when this drawer is nested.
- Type
boolean- Default
false
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerBackdropState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerBackdropState, children: Snippet | undefined]>
open boolean
- Name
open- Type
boolean
transitionStatus 'starting' | 'ending' | 'idle' | undefined
- Name
transitionStatus- Type
'starting' | 'ending' | 'idle' | undefined
data-open
data-closed
data-starting-style
data-ending-style
--drawer-swipe-progress
Viewport
A positioning container for the drawer popup that can be made scrollable. Renders a <div> element. It runs the swipe-to-dismiss gesture and snap point settling.
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerViewportState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerViewportState, children: Snippet | undefined]>
open boolean
- Name
open- Description
Whether the drawer is open.
- Type
boolean
transitionStatus 'starting' | 'ending' | 'idle' | undefined
- Name
transitionStatus- Description
Open/close motion phase.
- Type
'starting' | 'ending' | 'idle' | undefined
nested boolean
- Name
nested- Description
Whether the drawer is nested in another drawer.
- Type
boolean
nestedDialogOpen boolean
- Name
nestedDialogOpen- Description
Whether a nested drawer is open.
- Type
boolean
data-open
data-closed
data-nested
data-starting-style
data-ending-style
--drawer-keyboard-inset
Drawer.VirtualKeyboardProvider.Popup
A container for the drawer contents. Renders a <div> element.
initialFocus DialogFocusTarget —
- Name
- Description
Element to focus when the drawer opens. Defaults to the popup itself. Pass an element or a function. There is no ref object.
- Type
DialogFocusTarget
finalFocus DialogFocusTarget —
- Name
- Description
Element to focus when the drawer closes. Pass an element or a function. There is no ref object.
- Type
DialogFocusTarget
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerPopupState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerPopupState, children: Snippet | undefined]>
open boolean
- Name
open- Description
Whether the drawer is open.
- Type
boolean
transitionStatus 'starting' | 'ending' | 'idle' | undefined
- Name
transitionStatus- Description
Open/close motion phase.
- Type
'starting' | 'ending' | 'idle' | undefined
expanded boolean
- Name
expanded- Description
Whether the active snap point is the full-height one.
- Type
boolean
nested boolean
- Name
nested- Description
Whether the drawer is nested in a parent drawer.
- Type
boolean
nestedDrawerOpen boolean
- Name
nestedDrawerOpen- Description
Whether a nested drawer is open.
- Type
boolean
nestedDrawerSwiping boolean
- Name
nestedDrawerSwiping- Description
Whether a nested drawer is being swiped.
- Type
boolean
swipeDirection DrawerSwipeDirection
- Name
swipeDirection- Description
The direction a swipe dismisses the drawer in.
- Type
DrawerSwipeDirection
swiping boolean
- Name
swiping- Description
Whether the drawer is being swiped.
- Type
boolean
data-open
data-closed
data-expanded
data-nested-drawer-open
data-nested-drawer-swiping
data-swipe-direction
data-swipe-dismiss
data-swiping
data-starting-style
data-ending-style
--drawer-frontmost-height
--drawer-height
--drawer-snap-point-offset
--drawer-swipe-movement-x
--drawer-swipe-movement-y
--drawer-swipe-strength
--nested-drawers
Content
A container for the drawer contents. Renders a <div> element. Mouse and pen drags that start inside it select text instead of swiping the drawer.
render Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerContentState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLDivElement> & Record<symbol, Attachment<HTMLDivElement>>, state: DrawerContentState, children: Snippet | undefined]>
Title
A heading that labels the dialog. Renders an <h2> element.
render Snippet<[props: HTMLAttributes<HTMLHeadingElement> & Record<symbol, Attachment<HTMLHeadingElement>>, state: DialogTitleState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLHeadingElement> & Record<symbol, Attachment<HTMLHeadingElement>>, state: DialogTitleState, children: Snippet | undefined]>
Description
A paragraph with additional information about the dialog. Renders a <p> element.
render Snippet<[props: HTMLAttributes<HTMLParagraphElement> & Record<symbol, Attachment<HTMLParagraphElement>>, state: DialogDescriptionState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: HTMLAttributes<HTMLParagraphElement> & Record<symbol, Attachment<HTMLParagraphElement>>, state: DialogDescriptionState, children: Snippet | undefined]>
Close
A button that closes the dialog. Renders a <button> element.
nativeButton boolean true
- Name
- Description
falserenders throughrenderinstead of a native button.- Type
boolean- Default
true
render Snippet<[props: DialogTriggerHostProps, state: DialogCloseState, children: Snippet | undefined]> —
- Name
- Type
Snippet<[props: DialogTriggerHostProps, state: DialogCloseState, children: Snippet | undefined]>
disabled boolean
- Name
disabled- Type
boolean
data-disabled
createHandle
Creates a new handle to connect a Drawer.Root with detached Drawer.Trigger components.
Drawer.createHandle<Payload>() returns a Drawer.Handle<Payload>.
Handle
Controls a Drawer imperatively and associates detached Drawer.Trigger components with a Drawer.Root. Create one with Drawer.createHandle() and pass it to the handle prop of the root and of any triggers rendered outside of it.
The imperative methods take effect only while a root using this handle is mounted; calls made before a root attaches (or after it unmounts) are ignored.
| Property | Type | Description |
|---|---|---|
isOpen | boolean | Whether the dialog is currently open. Returns false while no root is attached to the handle. |
payload | Payload \| undefined | The payload of the trigger that opened the drawer, or the one passed to openWithPayload(). |
| Method | Description |
|---|---|
open(triggerId: string \| null): void | Opens the dialog, optionally associating it with a trigger. |
openWithPayload(payload: Payload): void | Opens the dialog with the given payload, without associating it with any trigger. |
close(): void | Closes the dialog. |
unmount(): void | Removes the popup at once, without waiting for the exit animation. |
Call these methods only in an event handler or an effect, not during rendering.