Skip to contents

Drawer

A panel that slides in from the edge of the screen.

import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

export default function ExampleDrawer() {
  return (
    <Drawer.Root swipeDirection="right">
      <Drawer.Trigger class={styles.Button}>Open drawer</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop class={styles.Backdrop} />
        <Drawer.Viewport class={styles.Viewport}>
          <Drawer.Popup class={styles.Popup}>
            <Drawer.Content class={styles.Content}>
              <Drawer.Title class={styles.Title}>Drawer</Drawer.Title>
              <Drawer.Description class={styles.Description}>
                This is a drawer that slides in from the side. You can swipe to dismiss it.
              </Drawer.Description>
              <div class={styles.Actions}>
                <Drawer.Close class={styles.Button}>Close</Drawer.Close>
              </div>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

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:

Anatomy
import { Drawer } from 'base-ui-solid/drawer';

<Drawer.Provider>
  <Drawer.IndentBackground />
  <Drawer.Indent>
    <Drawer.Root>
      <Drawer.Trigger />
      <Drawer.SwipeArea />
      <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.

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.

Uncontrolled drawer
<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.

Controlled drawer
import { createSignal } from 'solid-js';

const [open, setOpen] = createSignal(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.

Swipe directions
<Drawer.Root swipeDirection="right">
import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

export default function ExampleDrawer() {
  return (
    <Drawer.Root>
      <Drawer.Trigger class={styles.Button}>Open bottom drawer</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop class={styles.Backdrop} />
        <Drawer.Viewport class={styles.Viewport}>
          <Drawer.Popup class={styles.Popup}>
            <div class={styles.Handle} />
            <Drawer.Content class={styles.Content}>
              <Drawer.Title class={styles.Title}>Notifications</Drawer.Title>
              <Drawer.Description class={styles.Description}>
                You are all caught up. Good job!
              </Drawer.Description>
              <div class={styles.Actions}>
                <Drawer.Close class={styles.Button}>Close</Drawer.Close>
              </div>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

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.

import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

export default function ExampleDrawerNested() {
  return (
    <Drawer.Root>
      <Drawer.Trigger class={styles.Button}>Open drawer stack</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop class={styles.Backdrop} />
        <Drawer.Viewport class={styles.Viewport}>
          <Drawer.Popup class={styles.Popup}>
            <div class={styles.Handle} />
            <Drawer.Content class={styles.Content}>
              <Drawer.Title class={styles.Title}>Account</Drawer.Title>
              <Drawer.Description class={styles.Description}>
                Nested drawers can be styled to stack, while each drawer remains independently focus
                managed.
              </Drawer.Description>

              <div class={styles.Actions}>
                <div class={styles.ActionsLeft}>
                  <Drawer.Root>
                    <Drawer.Trigger class={styles.Button}>Security settings</Drawer.Trigger>
                    <Drawer.Portal>
                      <Drawer.Viewport class={styles.Viewport}>
                        <Drawer.Popup class={styles.Popup}>
                          <div class={styles.Handle} />
                          <Drawer.Content class={styles.Content}>
                            <Drawer.Title class={styles.Title}>Security</Drawer.Title>
                            <Drawer.Description class={styles.Description}>
                              Review sign-in activity and update your security preferences.
                            </Drawer.Description>

                            <ul class={styles.List}>
                              <li>Passkeys enabled</li>
                              <li>2FA via authenticator app</li>
                              <li>3 signed-in devices</li>
                            </ul>

                            <div class={styles.Actions}>
                              <div class={styles.ActionsLeft}>
                                <Drawer.Root>
                                  <Drawer.Trigger class={styles.Button}>
                                    Advanced options
                                  </Drawer.Trigger>
                                  <Drawer.Portal>
                                    <Drawer.Viewport class={styles.Viewport}>
                                      <Drawer.Popup class={styles.Popup}>
                                        <div class={styles.Handle} />
                                        <Drawer.Content class={styles.Content}>
                                          <Drawer.Title class={styles.Title}>Advanced</Drawer.Title>
                                          <Drawer.Description class={styles.Description}>
                                            This drawer is taller to demonstrate variable-height
                                            stacking.
                                          </Drawer.Description>

                                          <div class={styles.Field}>
                                            <label class={styles.Label} for="device-name">
                                              Device name
                                            </label>
                                            <input
                                              id="device-name"
                                              class={styles.Input}
                                              defaultValue="Personal laptop"
                                            />
                                          </div>

                                          <div class={styles.Field}>
                                            <label class={styles.Label} for="notes">
                                              Notes
                                            </label>
                                            <textarea
                                              id="notes"
                                              class={styles.Textarea}
                                              defaultValue="Rotate recovery codes and revoke older sessions."
                                              rows={3}
                                            />
                                          </div>

                                          <div class={styles.Actions}>
                                            <Drawer.Close class={styles.Button}>Done</Drawer.Close>
                                          </div>
                                        </Drawer.Content>
                                      </Drawer.Popup>
                                    </Drawer.Viewport>
                                  </Drawer.Portal>
                                </Drawer.Root>
                              </div>

                              <Drawer.Close class={styles.Button}>Close</Drawer.Close>
                            </div>
                          </Drawer.Content>
                        </Drawer.Popup>
                      </Drawer.Viewport>
                    </Drawer.Portal>
                  </Drawer.Root>
                </div>

                <Drawer.Close class={styles.Button}>Close</Drawer.Close>
              </div>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

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').

Snap points
const snapPoints = ['148px', 1];
const [snapPoint, setSnapPoint] = createSignal<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:

Snap point offset
.DrawerPopup {
  transform: translateY(calc(var(--drawer-snap-point-offset) + var(--drawer-swipe-movement-y)));
}
import type { JSX } from '@solidjs/web';
import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

const TOP_MARGIN_REM = 1;
const VISIBLE_SNAP_POINTS_REM = [30];

function toViewportSnapPoint(heightRem: number) {
  return `${heightRem + TOP_MARGIN_REM}rem`;
}

const snapPoints = [...VISIBLE_SNAP_POINTS_REM.map(toViewportSnapPoint), 1];

export default function ExampleDrawerSnapPoints() {
  return (
    <Drawer.Root snapPoints={snapPoints}>
      <Drawer.Trigger class={styles.Button}>Open snap drawer</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop class={styles.Backdrop} />
        <Drawer.Viewport class={styles.Viewport}>
          <Drawer.Popup
            class={styles.Popup}
            style={{ '--top-margin': `${TOP_MARGIN_REM}rem` } as JSX.CSSProperties}
          >
            <div class={styles.DragArea}>
              <div class={styles.Handle} />
              <Drawer.Title class={styles.Title}>Snap points</Drawer.Title>
            </div>
            <Drawer.Content class={styles.Scroll}>
              <div class={styles.Content}>
                <Drawer.Description class={styles.Description}>
                  Drag the sheet to snap between a compact peek and a near full-height view.
                </Drawer.Description>
                <div class={styles.Cards} aria-hidden="true">
                  {Array.from({ length: 20 }, (_, _index) => (
                    <div class={styles.Card} />
                  ))}
                </div>
                <div class={styles.Actions}>
                  <Drawer.Close class={styles.Button}>Close</Drawer.Close>
                </div>
              </div>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

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.
Virtual keyboard aware drawer
<Drawer.Root>
  <Drawer.VirtualKeyboardProvider>{/* ... */}</Drawer.VirtualKeyboardProvider>
</Drawer.Root>
import { For } from 'solid-js';
import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

const fields = [
  ['Name', 'Ada Lovelace'],
  ['Phone', '+1 (555) 123-4567'],
  ['Street address', '12 Computing Way'],
  ['Apartment', 'Unit 4B'],
  ['City', 'San Francisco'],
  ['Postal code', '94107'],
  ['Delivery window', 'After 6 PM'],
  ['Backup contact', 'Grace Hopper'],
];

export default function ExampleDrawerVirtualKeyboardAware() {
  return (
    <Drawer.Root>
      <Drawer.Trigger class={styles.Button}>Open keyboard-aware drawer</Drawer.Trigger>
      <Drawer.VirtualKeyboardProvider>
        <Drawer.Portal>
          <Drawer.Backdrop class={styles.Backdrop} />
          <Drawer.Viewport class={styles.Viewport}>
            <Drawer.Popup class={styles.Popup}>
              <div class={styles.Header}>
                <div class={styles.Handle} />
                <div class={styles.HeaderActions}>
                  <Drawer.Close class={`${styles.Button} ${styles.HeaderButton}`}>
                    Cancel
                  </Drawer.Close>
                  <Drawer.Title class={styles.Title}>Delivery details</Drawer.Title>
                  <Drawer.Close class={`${styles.Button} ${styles.HeaderButton}`}>
                    Save
                  </Drawer.Close>
                </div>
              </div>

              <Drawer.Content class={styles.Scroll}>
                <div class={styles.Form}>
                  <For each={fields}>
                    {([label, placeholder]) => (
                      <label class={styles.Field}>
                        <span class={styles.FieldLabel}>{label}</span>
                        <input class={styles.Input} placeholder={placeholder} type="text" />
                      </label>
                    )}
                  </For>

                  <label class={styles.Field}>
                    <span class={styles.FieldLabel}>Instructions</span>
                    <textarea
                      class={styles.Textarea}
                      placeholder="Gate code, drop-off spot, or anything else the driver should know"
                    />
                  </label>
                </div>
              </Drawer.Content>

              <div class={styles.FooterSlot}>
                <div class={styles.StickyFooter}>
                  <label class={styles.Composer}>
                    <span class={styles.FieldLabel}>Delivery note</span>
                    <input
                      class={styles.ComposerInput}
                      placeholder="Add a note for the driver"
                      type="text"
                    />
                  </label>
                </div>
              </div>
            </Drawer.Popup>
          </Drawer.Viewport>
        </Drawer.Portal>
      </Drawer.VirtualKeyboardProvider>
    </Drawer.Root>
  );
}

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

import { createSignal } from 'solid-js';
import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

export default function ExampleDrawer() {
  const [portalContainer, setPortalContainer] = createSignal<HTMLDivElement | null>(null);

  return (
    <Drawer.Provider>
      <div class={styles.Root} ref={setPortalContainer}>
        <Drawer.IndentBackground class={styles.IndentBackground} />
        <Drawer.Indent class={styles.Indent}>
          <div class={styles.Center}>
            <Drawer.Root modal={false}>
              <Drawer.Trigger class={styles.Button}>Open drawer</Drawer.Trigger>
              <Drawer.Portal container={portalContainer()}>
                <Drawer.Backdrop class={styles.Backdrop} />
                <Drawer.Viewport class={styles.Viewport}>
                  <Drawer.Popup class={styles.Popup}>
                    <div class={styles.Handle} />
                    <Drawer.Content class={styles.Content}>
                      <Drawer.Title class={styles.Title}>Notifications</Drawer.Title>
                      <Drawer.Description class={styles.Description}>
                        You are all caught up. Good job!
                      </Drawer.Description>
                      <div class={styles.Actions}>
                        <Drawer.Close class={styles.Button}>Close</Drawer.Close>
                      </div>
                    </Drawer.Content>
                  </Drawer.Popup>
                </Drawer.Viewport>
              </Drawer.Portal>
            </Drawer.Root>
          </div>
        </Drawer.Indent>
      </div>
    </Drawer.Provider>
  );
}

Non-modal

Set modal={false} to opt out of focus trapping and disablePointerDismissal to keep the drawer open on outside clicks.

import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

export default function ExampleDrawer() {
  return (
    <Drawer.Root swipeDirection="right" modal={false} disablePointerDismissal>
      <Drawer.Trigger class={styles.Button}>Open non-modal drawer</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Viewport class={styles.Viewport}>
          <Drawer.Popup class={styles.Popup}>
            <Drawer.Content class={styles.Content}>
              <Drawer.Title class={styles.Title}>Non-modal drawer</Drawer.Title>
              <Drawer.Description class={styles.Description}>
                This drawer does not trap focus and ignores outside clicks. Use the close button or
                swipe to dismiss it.
              </Drawer.Description>
              <div class={styles.Actions}>
                <Drawer.Close class={styles.Button}>Close</Drawer.Close>
              </div>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

Mobile navigation

You can build a full-screen mobile navigation sheet using Drawer parts, including a flick-to-dismiss from the top gesture.

import { For } from 'solid-js';
import type { JSX } from '@solidjs/web';
import { Drawer } from 'base-ui-solid/drawer';
import { ScrollArea } from 'base-ui-solid/scroll-area';
import styles from './index.module.css';

const ITEMS = [
  { href: '/solid/overview', label: 'Overview' },
  { href: '/solid/components', label: 'Components' },
  { href: '/solid/utils', label: 'Utilities' },
  { href: '/solid/overview/releases', label: 'Releases' },
] as const;

const LONG_LIST = [
  { href: '/solid/components/accordion', label: 'Accordion' },
  { href: '/solid/components/alert-dialog', label: 'Alert Dialog' },
  { href: '/solid/components/autocomplete', label: 'Autocomplete' },
  { href: '/solid/components/avatar', label: 'Avatar' },
  { href: '/solid/components/button', label: 'Button' },
  { href: '/solid/components/checkbox', label: 'Checkbox' },
  { href: '/solid/components/checkbox-group', label: 'Checkbox Group' },
  { href: '/solid/components/collapsible', label: 'Collapsible' },
  { href: '/solid/components/combobox', label: 'Combobox' },
  { href: '/solid/components/context-menu', label: 'Context Menu' },
  { href: '/solid/components/dialog', label: 'Dialog' },
  { href: '/solid/components/drawer', label: 'Drawer' },
  { href: '/solid/components/field', label: 'Field' },
  { href: '/solid/components/fieldset', label: 'Fieldset' },
  { href: '/solid/components/form', label: 'Form' },
  { href: '/solid/components/input', label: 'Input' },
  { href: '/solid/components/menu', label: 'Menu' },
  { href: '/solid/components/menubar', label: 'Menubar' },
  { href: '/solid/components/meter', label: 'Meter' },
  { href: '/solid/components/navigation-menu', label: 'Navigation Menu' },
  { href: '/solid/components/number-field', label: 'Number Field' },
  { href: '/solid/components/otp-field', label: 'OTP Field' },
  { href: '/solid/components/popover', label: 'Popover' },
  { href: '/solid/components/preview-card', label: 'Preview Card' },
  { href: '/solid/components/progress', label: 'Progress' },
  { href: '/solid/components/radio-group', label: 'Radio Group' },
  { href: '/solid/components/scroll-area', label: 'Scroll Area' },
  { href: '/solid/components/select', label: 'Select' },
  { href: '/solid/components/separator', label: 'Separator' },
  { href: '/solid/components/slider', label: 'Slider' },
  { href: '/solid/components/switch', label: 'Switch' },
  { href: '/solid/components/tabs', label: 'Tabs' },
  { href: '/solid/components/toast', label: 'Toast' },
  { href: '/solid/components/toggle', label: 'Toggle' },
  { href: '/solid/components/toggle-group', label: 'Toggle Group' },
  { href: '/solid/components/toolbar', label: 'Toolbar' },
  { href: '/solid/components/tooltip', label: 'Tooltip' },
] as const;

export default function ExampleDrawerMobileNav() {
  return (
    <Drawer.Root>
      <Drawer.Trigger class={styles.Button}>Open mobile menu</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop class={styles.Backdrop} />
        <Drawer.Viewport class={styles.Viewport}>
          <ScrollArea.Root style={{ position: undefined }} class={styles.ScrollAreaRoot}>
            <ScrollArea.Viewport class={styles.ScrollAreaViewport}>
              <ScrollArea.Content class={styles.ScrollContent}>
                <Drawer.Popup class={styles.Popup}>
                  <nav aria-label="Navigation" class={styles.Panel}>
                    <div class={styles.Header}>
                      <div aria-hidden="true" class={styles.HeaderSpacer} />
                      <div class={styles.Handle} />
                      <Drawer.Close aria-label="Close menu" class={styles.CloseButton}>
                        <XIcon />
                      </Drawer.Close>
                    </div>

                    <Drawer.Content class={styles.Content}>
                      <Drawer.Title class={styles.Title}>Menu</Drawer.Title>
                      <Drawer.Description class={styles.Description}>
                        Scroll the long list. Flick down from the top to dismiss.
                      </Drawer.Description>

                      <div class={styles.ScrollArea}>
                        <ul class={styles.List}>
                          <For each={ITEMS}>
                            {(item) => (
                              <li class={styles.Item}>
                                <a class={styles.Link} href={item.href}>
                                  {item.label}
                                </a>
                              </li>
                            )}
                          </For>
                        </ul>

                        <ul class={styles.LongList} aria-label="Component links">
                          <For each={LONG_LIST}>
                            {(item) => (
                              <li class={styles.Item}>
                                <a class={styles.Link} href={item.href}>
                                  {item.label}
                                </a>
                              </li>
                            )}
                          </For>
                        </ul>
                      </div>
                    </Drawer.Content>
                  </nav>
                </Drawer.Popup>
              </ScrollArea.Content>
            </ScrollArea.Viewport>
            <ScrollArea.Scrollbar class={styles.Scrollbar}>
              <ScrollArea.Thumb class={styles.ScrollbarThumb} />
            </ScrollArea.Scrollbar>
          </ScrollArea.Root>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

function XIcon(props: JSX.IntrinsicElements['svg']) {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      stroke-linecap="square"
      stroke-linejoin="round"
      {...props}
      style={
        typeof props.style === 'string'
          ? `display: block; ${props.style}`
          : { display: 'block', ...(typeof props.style === 'object' ? props.style : {}) }
      }
    >
      <path d="m2.5 2.5 11 11m-11 0 11-11" />
    </svg>
  );
}

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.

import { createSignal } from 'solid-js';
import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

export default function ExampleDrawerSwipeArea() {
  const [portalContainer, setPortalContainer] = createSignal<HTMLDivElement | null>(null);

  return (
    <div class={styles.Root} ref={setPortalContainer}>
      <Drawer.Root swipeDirection="right" modal={false}>
        <Drawer.SwipeArea class={styles.SwipeArea}>
          <span class={styles.SwipeLabel}>Swipe here</span>
        </Drawer.SwipeArea>
        <div class={styles.Center}>
          <div class={styles.Instructions}>
            <p class={styles.Hint}>Swipe from the right edge to open the drawer.</p>
          </div>
        </div>
        <Drawer.Portal container={portalContainer()}>
          <Drawer.Backdrop class={styles.Backdrop} />
          <Drawer.Viewport class={styles.Viewport}>
            <Drawer.Popup class={styles.Popup}>
              <Drawer.Content class={styles.Content}>
                <Drawer.Title class={styles.Title}>Library</Drawer.Title>
                <Drawer.Description class={styles.Description}>
                  Swipe from the edge whenever you want to jump back into your playlists.
                </Drawer.Description>
                <div class={styles.Actions}>
                  <Drawer.Close class={styles.Button}>Close</Drawer.Close>
                </div>
              </Drawer.Content>
            </Drawer.Popup>
          </Drawer.Viewport>
        </Drawer.Portal>
      </Drawer.Root>
    </div>
  );
}

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.

import { createSignal, createUniqueId } from 'solid-js';
import { AlertDialog } from 'base-ui-solid/alert-dialog';
import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

export default function ExampleDrawer() {
  const [drawerOpen, setDrawerOpen] = createSignal(false);
  const [confirmationOpen, setConfirmationOpen] = createSignal(false);
  const [textareaValue, setTextareaValue] = createSignal('');
  const titleId = createUniqueId();

  return (
    <Drawer.Root
      swipeDirection="right"
      open={drawerOpen()}
      onOpenChange={(open, eventDetails) => {
        // Show the close confirmation if there’s text in the textarea
        if (!open && textareaValue()) {
          eventDetails.cancel();
          setConfirmationOpen(true);
          return;
        }
        if (!open) {
          // Reset the textarea value
          setTextareaValue('');
        }
        setDrawerOpen(open);
      }}
    >
      <Drawer.Trigger class={styles.Button}>Tweet</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop class={styles.Backdrop} />
        <Drawer.Viewport class={styles.Viewport}>
          <Drawer.Popup
            class={`${styles.Popup} ${confirmationOpen() ? styles.PopupDimmed : ''}`.trim()}
          >
            <Drawer.Content class={styles.Content}>
              <Drawer.Title id={titleId} class={styles.Title}>
                New tweet
              </Drawer.Title>
              <form
                class={styles.TextareaContainer}
                onSubmit={(event) => {
                  event.preventDefault();
                  // Close the drawer when submitting
                  setTextareaValue('');
                  setDrawerOpen(false);
                }}
              >
                <textarea
                  aria-labelledby={titleId}
                  required
                  class={styles.Textarea}
                  placeholder="What’s on your mind?"
                  value={textareaValue()}
                  onInput={(event) => setTextareaValue(event.target.value)}
                />
                <div class={styles.Actions}>
                  <Drawer.Close class={styles.Button}>Cancel</Drawer.Close>
                  <button type="submit" class={styles.Button}>
                    Tweet
                  </button>
                </div>
              </form>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>

      {/* Confirmation dialog */}
      <AlertDialog.Root open={confirmationOpen()} onOpenChange={setConfirmationOpen}>
        <AlertDialog.Portal>
          <AlertDialog.Popup class={styles.AlertPopup}>
            <div class={styles.Intro}>
              <AlertDialog.Title class={styles.Title}>Discard tweet?</AlertDialog.Title>
              <AlertDialog.Description class={styles.Description}>
                Your tweet will be lost.
              </AlertDialog.Description>
            </div>
            <div class={styles.Actions}>
              <AlertDialog.Close class={styles.Button}>Go back</AlertDialog.Close>
              <button
                type="button"
                class={styles.Button}
                onClick={() => {
                  setConfirmationOpen(false);
                  setTextareaValue('');
                  setDrawerOpen(false);
                }}
              >
                Discard
              </button>
            </div>
          </AlertDialog.Popup>
        </AlertDialog.Portal>
      </AlertDialog.Root>
    </Drawer.Root>
  );
}

Action sheet with separate destructive action

This demo builds an action sheet with a grouped list of actions plus a separate destructive action button.

import { createSignal, For } from 'solid-js';
import { Drawer } from 'base-ui-solid/drawer';
import styles from './index.module.css';

const ACTIONS = ['Unfollow', 'Mute', 'Add to Favourites', 'Add to Close Friends', 'Restrict'];

export default function ExampleDrawerUncontained() {
  const [open, setOpen] = createSignal(false);

  return (
    <Drawer.Root open={open()} onOpenChange={setOpen}>
      <Drawer.Trigger class={styles.Button}>Open action sheet</Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Backdrop class={styles.Backdrop} />
        <Drawer.Viewport class={styles.Viewport}>
          <Drawer.Popup class={styles.Popup}>
            <Drawer.Content class={styles.Surface}>
              <Drawer.Title class={styles.VisuallyHidden}>Profile actions</Drawer.Title>
              <Drawer.Description class={styles.VisuallyHidden}>
                Choose an action for this user.
              </Drawer.Description>

              <ul class={styles.Actions} aria-label="Profile actions">
                <For each={ACTIONS}>
                  {(action, index) => (
                    <li class={styles.Action}>
                      {index() === 0 && (
                        <Drawer.Close class={styles.VisuallyHidden}>
                          Close action sheet
                        </Drawer.Close>
                      )}
                      <button
                        type="button"
                        class={styles.ActionButton}
                        onClick={() => setOpen(false)}
                      >
                        {action}
                      </button>
                    </li>
                  )}
                </For>
              </ul>
            </Drawer.Content>
            <div class={styles.DangerSurface}>
              <button type="button" class={styles.DangerButton} onClick={() => setOpen(false)}>
                Block User
              </button>
            </div>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

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.

Detached triggers
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>.

Detached triggers with payload
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.

Stack depth
.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.

Variable-height stacking
.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.

Nested content visibility
.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:

Swipe and snap offset
.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:

Swipe dismissal direction
.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.

Backdrop and release timing
.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

Provides a shared context for coordinating global Drawer UI, such as indent/background effects based on whether any Drawer is open. Doesn’t render its own HTML element.

Prop
Type
Default
childrenJSX.Element—
-JSX.Element
Provider.State
type DrawerProviderState = {};

IndentBackground

An element placed before <Drawer.Indent> to render a background layer that can be styled based on whether any drawer is open. Renders a <div> element.

Prop
Type
Default
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)
IndentBackground.State
type DrawerIndentBackgroundState = {
  /** Whether any drawer within the nearest <Drawer.Provider> is open. */
  active: boolean;
};

Indent

A wrapper element intended to contain your app’s main UI. Applies data-active when any drawer within the nearest <Drawer.Provider> is open. Renders a <div> element.

Prop
Type
Default
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)
Indent.State
type DrawerIndentState = {
  /** Whether any drawer within the nearest <Drawer.Provider> is open. */
  active: boolean;
};

Root

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

Prop
Type
Default
defaultOpenbooleanfalse
Whether the drawer is initially open. To render a controlled drawer, use the open prop instead.boolean
openboolean—
Whether the drawer is currently open.boolean
onOpenChangefunction—
Event handler called when the drawer is opened or closed.((open: boolean, eventDetails: Drawer.Root.ChangeEventDetails) => void)
snapPointsDrawerSnapPoint[]—
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').DrawerSnapPoint[]
defaultSnapPointUnion—
The initial snap point value when uncontrolled.DrawerSnapPoint | null
snapPointUnion—
The currently active snap point. Use with onSnapPointChange to control the snap point.DrawerSnapPoint | null
onSnapPointChangefunction—
Callback fired when the snap point changes.((snapPoint: DrawerSnapPoint | null, eventDetails: Drawer.Root.SnapPointChangeEventDetails) => void)
actionsRefRefObject<Drawer.Root.Actions | null>—
A ref to imperative actions. unmount: Ends the closing phase of the drawer after an externally controlled closing animation finishes. Call preventUnmountOnClose() in onOpenChange first, otherwise the drawer completes closing on its own. Whether it leaves the DOM is decided by keepMounted on the portal.close: Closes the drawer imperatively when called.RefObject<Drawer.Root.Actions | null>
defaultTriggerIdUnion—
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.string | null
disablePointerDismissalbooleanfalse
Whether to prevent the drawer from closing on outside presses. For non-modal drawers, this also prevents the drawer from closing when focus moves outside of it.boolean
handleDrawer.Handle<Payload>—
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.Drawer.Handle<Payload>
modalUniontrue
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.boolean | 'trap-focus'
onOpenChangeCompletefunction—
Event handler called after any animations complete when the drawer is opened or closed.((open: boolean) => void)
snapToSequentialPointsbooleanfalse
Disables velocity-based snap skipping so drag distance determines the next snap point.boolean
swipeDirectionDrawerSwipeDirection'down'
The swipe direction used to dismiss the drawer.DrawerSwipeDirection
triggerIdUnion—
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 (that is, when the open prop is not set).string | null
childrenUnion—
The content of the drawer.JSX.Element | PayloadChildRenderFunction<Payload>
Root.State
type DrawerRootState = {};
Root.Actions
type DrawerRootActions = { unmount: () => void; close: () => void };
Root.ChangeEventReason
type DrawerRootChangeEventReason =
  | 'trigger-press'
  | 'outside-press'
  | 'escape-key'
  | 'close-watcher'
  | 'close-press'
  | 'focus-out'
  | 'imperative-action'
  | 'swipe'
  | 'none';
Root.ChangeEventDetails
type DrawerRootChangeEventDetails = (
  | { reason: 'trigger-press'; event: KeyboardEvent | MouseEvent | TouchEvent | PointerEvent }
  | { reason: 'outside-press'; event: MouseEvent | TouchEvent | PointerEvent }
  | { reason: 'escape-key'; event: KeyboardEvent }
  | { reason: 'close-press'; event: KeyboardEvent | MouseEvent | PointerEvent }
  | { reason: 'focus-out'; event: FocusEvent | KeyboardEvent }
  | { reason: 'imperative-action'; event: Event }
  | { reason: 'none'; event: Event }
  | { reason: 'close-watcher'; event: Event }
  | { reason: 'swipe'; event: TouchEvent | PointerEvent }
) & {
  /** Cancels Base UI from handling the event. */
  cancel: () => void;
  /** Allows the event to propagate in cases where Base UI will stop the propagation. */
  allowPropagation: () => void;
  /** Indicates whether the event has been canceled. */
  isCanceled: boolean;
  /** Indicates whether the event is allowed to propagate. */
  isPropagationAllowed: boolean;
  /** The element that triggered the event, if applicable. */
  trigger: Element | undefined;
  /** Prevents the popup from unmounting until the `unmount` action is called. */
  preventUnmountOnClose: () => void;
};
Root.SnapPoint
type DrawerRootSnapPoint = number | string;
Root.SnapPointChangeEventDetails
type DrawerRootSnapPointChangeEventDetails = (
  | { reason: 'trigger-press'; event: KeyboardEvent | MouseEvent | TouchEvent | PointerEvent }
  | { reason: 'outside-press'; event: MouseEvent | TouchEvent | PointerEvent }
  | { reason: 'escape-key'; event: KeyboardEvent }
  | { reason: 'close-press'; event: KeyboardEvent | MouseEvent | PointerEvent }
  | { reason: 'focus-out'; event: FocusEvent | KeyboardEvent }
  | { reason: 'imperative-action'; event: Event }
  | { reason: 'none'; event: Event }
  | { reason: 'close-watcher'; event: Event }
  | { reason: 'swipe'; event: TouchEvent | PointerEvent }
) & {
  /** Cancels Base UI from handling the event. */
  cancel: () => void;
  /** Allows the event to propagate in cases where Base UI will stop the propagation. */
  allowPropagation: () => void;
  /** Indicates whether the event has been canceled. */
  isCanceled: boolean;
  /** Indicates whether the event is allowed to propagate. */
  isPropagationAllowed: boolean;
  /** The element that triggered the event, if applicable. */
  trigger: Element | undefined;
};
Root.SnapPointChangeEventReason
type DrawerRootSnapPointChangeEventReason =
  | 'trigger-press'
  | 'outside-press'
  | 'escape-key'
  | 'close-watcher'
  | 'close-press'
  | 'focus-out'
  | 'imperative-action'
  | 'swipe'
  | 'none';

Trigger

A button that opens the drawer. Renders a <button> element.

Prop
Type
Default
handleDrawer.Handle<Payload>—
A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method.Drawer.Handle<Payload>
nativeButtonbooleantrue
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).boolean
payloadPayload—
A payload to pass to the drawer when it is opened.Payload
idstring—
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).string
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)

Data attributes

Name
Type
Default
data-popup-open-—
Present when the corresponding dialog is open.-
data-disabled-—
Present when the trigger is disabled.-
Attribute
Description
data-popup-open
Present when the corresponding dialog is open.
data-disabled
Present when the trigger is disabled.
Trigger.State
type DrawerTriggerState = {
  /** Whether the trigger is currently disabled. */
  disabled: boolean;
  /** Whether the drawer is currently open and was opened by this trigger. */
  open: boolean;
};

SwipeArea

An invisible area that listens for swipe gestures to open the drawer. Renders a <div> element.

Prop
Type
Default
swipeDirectionDrawerSwipeDirection—
The swipe direction that opens the drawer. Defaults to the opposite of Drawer.Root swipeDirection.DrawerSwipeDirection
disabledbooleanfalse
Whether the swipe area is disabled.boolean
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)

Data attributes

Name
Type
Default
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-directionUnion—
Indicates the swipe direction.'up' | 'down' | 'left' | 'right'
data-swiping-—
Present when the drawer is being swiped.-
Attribute
Description
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.
SwipeArea.State
type DrawerSwipeAreaState = {
  /** Whether the drawer is currently open. */
  open: boolean;
  /** Whether the swipe area is currently being swiped. */
  swiping: boolean;
  /** The swipe direction that opens the drawer. */
  swipeDirection: SwipeDirection;
  /** Whether the swipe area is disabled. */
  disabled: boolean;
};

VirtualKeyboardProvider

Provides keyboard-aware focus and scroll handling for bottom-sheet drawers with form fields.

Prop
Type
Default
childrenJSX.Element—
-JSX.Element
VirtualKeyboardProvider.State
type DrawerVirtualKeyboardProviderState = {};

Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div> element.

Prop
Type
Default
containerUnion—
A parent element to render the portal element into.HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
keepMountedbooleanfalse
Whether to keep the portal mounted in the DOM while the popup is hidden.boolean
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)
Portal.State
type DrawerPortalState = {};

Backdrop

An overlay displayed beneath the popup. Renders a <div> element.

Prop
Type
Default
forceRenderbooleanfalse
Whether the backdrop is forced to render even when nested.boolean
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)

Data attributes

Name
Type
Default
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.-
Attribute
Description
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.

CSS variables

Name
Type
Default
--drawer-swipe-progressnumber—
The swipe progress of the drawer gesture.number
CSS Variable
Description
--drawer-swipe-progress
The swipe progress of the drawer gesture.
Backdrop.State
type DrawerBackdropState = {
  /** Whether the drawer is currently open. */
  open: boolean;
  /** The transition status of the component. */
  transitionStatus: TransitionStatus;
};

Viewport

A positioning container for the drawer popup that can be made scrollable. Renders a <div> element.

Prop
Type
Default
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)

Data attributes

Name
Type
Default
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.-
Attribute
Description
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.

CSS variables

Name
Type
Default
--drawer-keyboard-insetCSS—
The software keyboard inset, measured from the bottom edge of the layout viewport. Present only when the drawer is wrapped in Drawer.VirtualKeyboardProvider.CSS
CSS Variable
Description
--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.
Viewport.State
type DrawerViewportState = {
  /** Whether the drawer is currently open. */
  open: boolean;
  /** The transition status of the component. */
  transitionStatus: TransitionStatus;
  /** Whether the drawer is nested within another drawer. */
  nested: boolean;
  /** Whether the drawer has nested drawers open. */
  nestedDialogOpen: boolean;
};

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

Prop
Type
Default
initialFocusfunction—
Determines the element to focus when the drawer is opened. false: Do not move focus.true: Move focus based on the default behavior (first tabbable element or popup).RefObject: Move focus to the ref element.function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, or false/undefined to do nothing.boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)
finalFocusfunction—
Determines the element to focus when the drawer is closed. false: Do not move focus.true: Move focus based on the default behavior (trigger or previously focused element).RefObject: Move focus to the ref element.function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, or false/undefined to do nothing.boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)

Data attributes

Name
Type
Default
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-directionUnion—
Indicates the swipe direction.'up' | 'down' | 'left' | 'right'
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.-
Attribute
Description
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.

CSS variables

Name
Type
Default
--drawer-frontmost-heightCSS—
The height of the frontmost open drawer in the current nested drawer stack.CSS
--drawer-heightCSS—
The height of the drawer popup.CSS
--drawer-snap-point-offsetCSS—
The snap point offset used for translating the drawer.CSS
--drawer-swipe-movement-xCSS—
The swipe movement on the X axis.CSS
--drawer-swipe-movement-yCSS—
The swipe movement on the Y axis.CSS
--drawer-swipe-strengthnumber—
A scalar (0.1-1) used to scale the swipe release transition duration in CSS.number
--nested-drawersnumber—
The number of nested drawers that are currently open.number
CSS Variable
Description
--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.
Popup.State
type DrawerPopupState = {
  /** Whether the drawer is currently open. */
  open: boolean;
  /** The transition status of the component. */
  transitionStatus: TransitionStatus;
  /** Whether the active snap point is the full-height expanded state. */
  expanded: boolean;
  /** Whether the drawer is nested within a parent drawer. */
  nested: boolean;
  /** Whether the drawer has nested drawers open. */
  nestedDrawerOpen: boolean;
  /** Whether a nested drawer is currently being swiped. */
  nestedDrawerSwiping: boolean;
  /** The swipe direction used to dismiss the drawer. */
  swipeDirection: DrawerSwipeDirection;
  /** Whether the drawer is being swiped. */
  swiping: boolean;
};

Content

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

Prop
Type
Default
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)
Content.State
type DrawerContentState = {};

Title

A heading that labels the drawer. Renders an <h2> element.

Prop
Type
Default
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)
Title.State
type DrawerTitleState = {};

Description

A paragraph with additional information about the drawer. Renders a <p> element.

Prop
Type
Default
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)
Description.State
type DrawerDescriptionState = {};

Close

A button that closes the drawer. Renders a <button> element.

Prop
Type
Default
nativeButtonbooleantrue
Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).boolean
classfunction—
CSS class applied to the element, or a function that returns a class based on the component’s state.JSX.ClassValue | ((state) => JSX.ClassValue)
stylefunction—
Style applied to the element, or a function that returns a style object based on the component’s state.JSX.CSSProperties | string | ((state) => JSX.CSSProperties | string | undefined)
renderfunction—
Replace the default element with a tag name, component, or render function.keyof JSX.IntrinsicElements | Component | ((props, state) => JSX.Element)

Data attributes

Name
Type
Default
data-disabled-—
Present when the button is disabled.-
Attribute
Description
data-disabled
Present when the button is disabled.
Close.State
type DrawerCloseState = {
  /** Whether the button is currently disabled. */
  disabled: boolean;
};

createHandle

Creates a new handle to connect a Drawer.Root with detached Drawer.Trigger components.

createHandle
type ReturnValue = 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.

Handle
function open(triggerId: string | null): void;
Handle
function openWithPayload(payload: Payload): void;
Handle
function close(): void;