Skip to contents

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 to position: fixed — positioned against the popup, since its transform contains fixed descendants — and adds the inset to its bottom padding.
  • Always include the 0px fallback: the provider only sets --drawer-keyboard-inset while the keyboard is aligned, so a bare var(--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.Provider is open.

Type
boolean
data-active
Present when a drawer in the provider is open.
data-inactive
Present when no drawer in the provider is open.

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.Provider is open.

Type
boolean
data-active
Present when a drawer in the provider is open.
data-inactive
Present when no drawer in the provider is open.
--drawer-swipe-progress
The frontmost drawer's swipe progress, from 0 to 1.
--drawer-height
The frontmost drawer's height, while a drawer is open.

Root

Groups all parts of the drawer. Doesn't render its own HTML element.

onOpenChange function —
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'
Description

The direction a swipe dismisses the drawer in.

Type
DrawerSwipeDirection
Default
'down'
snapPoints DrawerSnapPoint[] —
Description

Snap points that size the drawer. Numbers from 0 to 1 are fractions of the viewport height. Larger numbers are pixels. Strings take px or rem units, 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 —
Description

The active snap point. Use bind:snapPoint to share it with the parent. A value outside snapPoints resolves to the closest snap point.

Type
DrawerSnapPoint | null
Binding
bind:snapPoint
defaultSnapPoint DrawerSnapPoint | null —
Description

The snap point the drawer starts at when snapPoint is unset and returns to when it closes. Defaults to the first of snapPoints.

Type
DrawerSnapPoint | null
onSnapPointChange function —
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 }]> —
Type
Snippet<[{ payload: Payload | undefined }]>
modal boolean | 'trap-focus' true
Name
Description

true traps focus, locks page scroll, and blocks outside pointer interaction. false leaves 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:open to share it with the parent. A value passed without bind: holds until the parent changes it.

Type
boolean
Default
false
Binding
bind:open
onOpenChangeComplete function —
Description

Called after the open or close animation finishes.

Type
(open: boolean) => void
triggerId string | null null
Description

Id of the trigger that owns the dialog. Use bind:triggerId to share it with the parent. Pass it with an initial open to name the trigger the dialog starts from. An id written by a trigger press resets to null once 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
Description

false renders through render instead 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
Present when the corresponding dialog is open.
data-disabled
Present when the trigger is disabled.

SwipeArea

An invisible area that opens the drawer with a swipe. Renders a <div> element.

disabled boolean | null false
Description

Ignore swipes.

Type
boolean | null
Default
false
swipeDirection DrawerSwipeDirection —
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
Present when the drawer is open.
data-closed
Present when the drawer is closed.
data-disabled
Present when the swipe area is disabled.
data-swipe-direction
Indicates the swipe direction.
data-swiping
Present when the drawer is being swiped.

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
Description

Keep the portal mounted while the popup is closed.

Type
boolean
Default
false
container HTMLElement | ShadowRoot | null —
Description

Element the portal is appended to. undefined uses the parent portal or document.body. null waits 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
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
Present when the drawer is open.
data-closed
Present when the drawer is closed.
data-starting-style
Present when the drawer begins animating in.
data-ending-style
Present when the drawer is animating out.
--drawer-swipe-progress
The swipe progress of the drawer gesture.

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
Present when the drawer is open.
data-closed
Present when the drawer is closed.
data-nested
Present when the drawer is nested within another drawer.
data-starting-style
Present when the drawer begins animating in.
data-ending-style
Present when the drawer is animating out.
--drawer-keyboard-inset
The software keyboard inset, measured from the bottom edge of the layout viewport. Present only when the drawer is wrapped in Drawer.VirtualKeyboardProvider.

A container for the drawer contents. Renders a <div> element.

initialFocus DialogFocusTarget —
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 —
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
Present when the drawer is open.
data-closed
Present when the drawer is closed.
data-expanded
Present when the drawer is at the expanded (full-height) snap point.
data-nested-drawer-open
Present when a nested drawer is open.
data-nested-drawer-swiping
Present when a nested drawer is being swiped.
data-swipe-direction
Indicates the swipe direction.
data-swipe-dismiss
Present when the drawer is dismissed by swiping.
data-swiping
Present when the drawer is being swiped.
data-starting-style
Present when the drawer begins animating in.
data-ending-style
Present when the drawer is animating out.
--drawer-frontmost-height
The height of the frontmost open drawer in the current nested drawer stack.
--drawer-height
The height of the drawer popup.
--drawer-snap-point-offset
The snap point offset used for translating the drawer.
--drawer-swipe-movement-x
The swipe movement on the X axis.
--drawer-swipe-movement-y
The swipe movement on the Y axis.
--drawer-swipe-strength
A scalar (0.1-1) used to scale the swipe release transition duration in CSS.
--nested-drawers
The number of nested drawers that are currently open.

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
Description

false renders through render instead 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
Present when the button is 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.

PropertyTypeDescription
isOpenbooleanWhether the dialog is currently open. Returns false while no root is attached to the handle.
payloadPayload \| undefinedThe payload of the trigger that opened the drawer, or the one passed to openWithPayload().
MethodDescription
open(triggerId: string \| null): voidOpens the dialog, optionally associating it with a trigger.
openWithPayload(payload: Payload): voidOpens the dialog with the given payload, without associating it with any trigger.
close(): voidCloses the dialog.
unmount(): voidRemoves the popup at once, without waiting for the exit animation.

Call these methods only in an event handler or an effect, not during rendering.