HextaUI

Drawer

A panel that slides in from any edge and follows your finger, with snap points, a working handle and nested drawers that stack.

"use client"

import * as React from "react"
import { IconMinus, IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { NumberFlow } from "@/components/ui/number-flow"

export function DrawerDemo() {
  const [goal, setGoal] = React.useState(350)

  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline" />}>
        Set daily goal
      </DrawerTrigger>
      <DrawerContent>
        <div className="mx-auto flex w-full max-w-sm flex-col">
          <DrawerHeader>
            <DrawerTitle>Move goal</DrawerTitle>
            <DrawerDescription>Set your daily activity goal.</DrawerDescription>
          </DrawerHeader>
          <div className="flex items-center justify-center gap-6 p-6">
            <Button
              variant="outline"
              size="icon"
              aria-label="Decrease goal"
              disabled={goal <= 200}
              onClick={() => setGoal(goal - 10)}
            >
              <IconMinus />
            </Button>
            <div className="flex min-w-32 flex-col items-center">
              <NumberFlow
                value={goal}
                className="text-6xl font-semibold tracking-tight"
              />
              <span className="text-xs text-muted-foreground uppercase">
                Calories / day
              </span>
            </div>
            <Button
              variant="outline"
              size="icon"
              aria-label="Increase goal"
              disabled={goal >= 600}
              onClick={() => setGoal(goal + 10)}
            >
              <IconPlus />
            </Button>
          </div>
          <DrawerFooter>
            <DrawerClose render={<Button />}>Save goal</DrawerClose>
            <DrawerClose render={<Button variant="outline" />}>
              Cancel
            </DrawerClose>
          </DrawerFooter>
        </div>
      </DrawerContent>
    </Drawer>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/drawer.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
<Drawer>
  <DrawerTrigger render={<Button variant="outline" />}>Open</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Are you absolutely sure?</DrawerTitle>
      <DrawerDescription>This action cannot be undone.</DrawerDescription>
    </DrawerHeader>
    <DrawerBody>{/* content */}</DrawerBody>
    <DrawerFooter>
      <Button>Submit</Button>
      <DrawerClose render={<Button variant="outline" />}>Cancel</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
Drawer
├── DrawerTrigger
└── DrawerContent
    ├── DrawerHeader
    │   ├── DrawerTitle
    │   └── DrawerDescription
    ├── DrawerBody
    └── DrawerFooter
        └── DrawerClose

Directions

Set swipeDirection on <Drawer /> to pick the edge. The drawer opens from that edge and swipes back towards it. Top and bottom drawers show a handle by default.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

const directions = [
  { value: "up", label: "Top" },
  { value: "right", label: "Right" },
  { value: "down", label: "Bottom" },
  { value: "left", label: "Left" },
] as const

export function DrawerDirections() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      {directions.map(({ value, label }) => (
        <Drawer key={value} swipeDirection={value}>
          <DrawerTrigger render={<Button variant="outline" />}>
            {label}
          </DrawerTrigger>
          <DrawerContent>
            <DrawerHeader>
              <DrawerTitle>Swipe {value} to close</DrawerTitle>
              <DrawerDescription>
                The drawer follows your finger and keeps the momentum when you
                let go.
              </DrawerDescription>
            </DrawerHeader>
            <DrawerFooter>
              <DrawerClose render={<Button variant="outline" />}>
                Close
              </DrawerClose>
            </DrawerFooter>
          </DrawerContent>
        </Drawer>
      ))}
    </div>
  )
}

Snap points

Pass snapPoints to rest a bottom drawer at preset heights. Numbers from 0 to 1 are fractions of the viewport, larger numbers are pixels, and strings take px or rem. The visible part always fits its content, so nothing hides below the screen.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerBody,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

const snapPoints = ["20rem", 1]

const places = [
  "Blue Bottle Coffee",
  "Tartine Bakery",
  "Mission Dolores Park",
  "Ferry Building",
  "Golden Gate Park",
  "Lands End Trail",
  "Twin Peaks",
  "Coit Tower",
  "Alamo Square",
  "Baker Beach",
  "Crissy Field",
  "Japanese Tea Garden",
]

export function DrawerSnapPoints() {
  return (
    <Drawer snapPoints={snapPoints}>
      <DrawerTrigger render={<Button variant="outline" />}>
        Nearby places
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Nearby places</DrawerTitle>
          <DrawerDescription>
            Drag up to see the full list, or down to close.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerBody>
          <ul className="flex flex-col divide-y">
            {places.map((place) => (
              <li key={place} className="py-3 text-sm">
                {place}
              </li>
            ))}
          </ul>
        </DrawerBody>
      </DrawerContent>
    </Drawer>
  )
}

Scrollable content

<DrawerBody /> scrolls on its own, so the header and footer stay in place. Swiping only starts once the body is scrolled back to the top.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

const sections = Array.from({ length: 12 }, (_, index) => index + 1)

export function DrawerScrollable() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline" />}>
        Terms of service
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Terms of service</DrawerTitle>
          <DrawerDescription>Last updated October 2026.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody>
          <div className="flex flex-col gap-4 text-sm text-muted-foreground">
            {sections.map((section) => (
              <p key={section}>
                {section}. By using the service you agree to these terms. We may
                update them from time to time, and we will let you know when
                something important changes. Keep your account details safe and
                tell us if you think someone else has access.
              </p>
            ))}
          </div>
        </DrawerBody>
        <DrawerFooter>
          <DrawerClose render={<Button />}>Accept</DrawerClose>
          <DrawerClose render={<Button variant="outline" />}>
            Decline
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Forms and the keyboard

Wrap <DrawerContent /> in <DrawerVirtualKeyboardProvider /> when a bottom drawer holds text fields. On phones, the focused field scrolls into view above the software keyboard instead of hiding behind it. Keep the fields in <DrawerBody /> so the header and footer stay put.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
  DrawerVirtualKeyboardProvider,
} from "@/components/ui/drawer"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export function DrawerKeyboard() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline" />}>
        Delivery details
      </DrawerTrigger>
      <DrawerVirtualKeyboardProvider>
        <DrawerContent>
          <DrawerHeader>
            <DrawerTitle>Delivery details</DrawerTitle>
            <DrawerDescription>
              Tap a field on your phone. It stays above the keyboard.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerBody>
            <FieldGroup>
              <Field>
                <FieldLabel>Name</FieldLabel>
                <Input name="name" autoComplete="name" />
              </Field>
              <Field>
                <FieldLabel>Phone</FieldLabel>
                <Input name="phone" type="tel" autoComplete="tel" />
              </Field>
              <Field>
                <FieldLabel>Street address</FieldLabel>
                <Input name="street" autoComplete="street-address" />
              </Field>
              <Field>
                <FieldLabel>Apartment</FieldLabel>
                <Input name="apartment" />
              </Field>
              <Field>
                <FieldLabel>City</FieldLabel>
                <Input name="city" autoComplete="address-level2" />
              </Field>
              <Field>
                <FieldLabel>Postal code</FieldLabel>
                <Input name="postal" autoComplete="postal-code" />
              </Field>
            </FieldGroup>
          </DrawerBody>
          <DrawerFooter>
            <DrawerClose render={<Button />}>Save address</DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </DrawerVirtualKeyboardProvider>
    </Drawer>
  )
}

Nested

A drawer opened from a drawer on the same edge stacks on top. The ones behind shrink, peek out above it and follow your finger as you swipe the top one away.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

export function DrawerNested() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline" />}>
        Choose a plan
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Choose a plan</DrawerTitle>
          <DrawerDescription>
            Compare plans, then open the details of the one you like.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerFooter>
          <Drawer>
            <DrawerTrigger render={<Button />}>Pro plan details</DrawerTrigger>
            <DrawerContent>
              <DrawerHeader>
                <DrawerTitle>Pro plan</DrawerTitle>
                <DrawerDescription>
                  The first drawer steps back behind this one.
                </DrawerDescription>
              </DrawerHeader>
              <DrawerFooter>
                <Drawer>
                  <DrawerTrigger render={<Button />}>Checkout</DrawerTrigger>
                  <DrawerContent>
                    <DrawerHeader>
                      <DrawerTitle>Checkout</DrawerTitle>
                      <DrawerDescription>
                        Swipe down to go back one step.
                      </DrawerDescription>
                    </DrawerHeader>
                    <DrawerFooter>
                      <DrawerClose render={<Button variant="outline" />}>
                        Back
                      </DrawerClose>
                    </DrawerFooter>
                  </DrawerContent>
                </Drawer>
                <DrawerClose render={<Button variant="outline" />}>
                  Back
                </DrawerClose>
              </DrawerFooter>
            </DrawerContent>
          </Drawer>
          <DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Confirm from a drawer

Dialogs, alert dialogs, sheets and drawers on another edge layer on top instead of stacking. The drawer steps back and a lighter backdrop covers it.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

export function DrawerConfirm() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline" />}>
        Photo options
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>IMG_2048.heic</DrawerTitle>
          <DrawerDescription>Taken on October 3, 2026.</DrawerDescription>
        </DrawerHeader>
        <DrawerFooter>
          <DrawerClose render={<Button variant="outline" />}>Share</DrawerClose>
          <AlertDialog>
            <AlertDialogTrigger render={<Button variant="destructive" />}>
              Delete photo
            </AlertDialogTrigger>
            <AlertDialogContent>
              <AlertDialogHeader>
                <AlertDialogTitle>Delete “IMG_2048.heic”?</AlertDialogTitle>
                <AlertDialogDescription>
                  It will be removed from all your devices.
                </AlertDialogDescription>
              </AlertDialogHeader>
              <AlertDialogFooter>
                <AlertDialogCancel>Cancel</AlertDialogCancel>
                <AlertDialogAction variant="destructive">
                  Delete
                </AlertDialogAction>
              </AlertDialogFooter>
            </AlertDialogContent>
          </AlertDialog>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Responsive

Change swipeDirection with a media query to show a side panel on desktop and a bottom sheet on phones.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

const query = "(min-width: 768px)"

function subscribe(onChange: () => void) {
  const media = window.matchMedia(query)
  media.addEventListener("change", onChange)
  return () => media.removeEventListener("change", onChange)
}

function useIsDesktop() {
  return React.useSyncExternalStore(
    subscribe,
    () => window.matchMedia(query).matches,
    () => false
  )
}

export function DrawerResponsive() {
  const isDesktop = useIsDesktop()

  return (
    <Drawer swipeDirection={isDesktop ? "right" : "down"}>
      <DrawerTrigger render={<Button variant="outline" />}>
        Notifications
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Notifications</DrawerTitle>
          <DrawerDescription>
            A side panel on desktop and a bottom sheet on phones.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerFooter>
          <DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Non-modal

With modal={false} there is no backdrop, the page keeps scrolling and focus can leave the drawer.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

export function DrawerNonModal() {
  return (
    <Drawer modal={false} swipeDirection="right">
      <DrawerTrigger render={<Button variant="outline" />}>
        Open inspector
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Inspector</DrawerTitle>
          <DrawerDescription>
            The page stays scrollable and clickable while this is open.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerFooter>
          <DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Controlled

Pass open and onOpenChange to open it from anywhere, without a trigger.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
} from "@/components/ui/drawer"

export function DrawerControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open from anywhere
      </Button>
      <Drawer open={open} onOpenChange={setOpen}>
        <DrawerContent>
          <DrawerHeader>
            <DrawerTitle>Controlled drawer</DrawerTitle>
            <DrawerDescription>
              Opened from a plain button, without a trigger.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <Button variant="outline" onClick={() => setOpen(false)}>
              Close
            </Button>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
    </>
  )
}

Detached triggers

Share one drawer between several triggers with createDrawerHandle. Each trigger passes a payload that the drawer renders through a function child.

"use client"

import { Button } from "@/components/ui/button"
import {
  createDrawerHandle,
  Drawer,
  DrawerBody,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

const contacts = createDrawerHandle<{ name: string; email: string }>()

export function DrawerDetached() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <DrawerTrigger
        handle={contacts}
        payload={{ name: "Olivia Martin", email: "[email protected]" }}
        render={<Button variant="outline" size="sm" />}
      >
        Olivia
      </DrawerTrigger>
      <DrawerTrigger
        handle={contacts}
        payload={{ name: "Jackson Lee", email: "[email protected]" }}
        render={<Button variant="outline" size="sm" />}
      >
        Jackson
      </DrawerTrigger>
      <Drawer handle={contacts}>
        {({ payload }) => (
          <DrawerContent>
            <DrawerHeader>
              <DrawerTitle>{payload?.name}</DrawerTitle>
              <DrawerDescription>{payload?.email}</DrawerDescription>
            </DrawerHeader>
            <DrawerBody>
              <p className="text-sm text-muted-foreground">
                One drawer, opened by either trigger.
              </p>
            </DrawerBody>
          </DrawerContent>
        )}
      </Drawer>
    </div>
  )
}

Right to left

Pass dir="rtl" to <DrawerContent /> to mirror its content. swipeDirection names a physical edge, so "left" stays on the left and the handle stays on the inner edge.

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"

export function DrawerRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap justify-center gap-2">
      <Drawer>
        <DrawerTrigger render={<Button variant="outline" />}>
          من الأسفل
        </DrawerTrigger>
        <DrawerContent dir="rtl">
          <DrawerHeader>
            <DrawerTitle>تعديل الملف الشخصي</DrawerTitle>
            <DrawerDescription>
              يتم حفظ التغييرات عند النقر على حفظ.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <DrawerClose render={<Button />}>حفظ</DrawerClose>
            <DrawerClose render={<Button variant="outline" />}>
              إلغاء
            </DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
      <Drawer swipeDirection="left" showSwipeHandle>
        <DrawerTrigger render={<Button variant="outline" />}>
          من اليسار
        </DrawerTrigger>
        <DrawerContent dir="rtl">
          <DrawerHeader>
            <DrawerTitle>القائمة</DrawerTitle>
            <DrawerDescription>اسحب إلى اليسار للإغلاق.</DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <DrawerClose render={<Button variant="outline" />}>
              إغلاق
            </DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
    </div>
  )
}
KeyAction
EnterSpaceOn the trigger, opens the drawer and moves focus inside it.
TabShift + TabMoves between focusable elements. Focus stays inside a modal drawer.
EscCloses the topmost drawer and returns focus to its trigger.
  • The drawer is a dialog. <DrawerTitle /> labels it and <DrawerDescription /> describes it, so always include a title.
  • Swiping is never the only way out: Escape, the backdrop and a <DrawerClose /> button all close it too.
  • The handle is decorative and hidden from assistive tech. With a mouse, text inside the drawer can be selected without dragging it.
  • With reduced motion enabled, the drawer fades in and out instead of sliding. Dragging still follows the pointer.

Built on the Base UI drawer. Every part accepts the props of the primitive it wraps.

PropTypeDefault
swipeDirectionThe edge it opens from and the direction that dismisses it.
"up" | "down" | "left" | "right""down"
showSwipeHandleShows the handle. Defaults to true for up and down, false for left and right.
boolean–
snapPointsHeights a vertical drawer can rest at. 0–1 is a fraction of the viewport, >1 is pixels, strings take px or rem.
(number | string)[]–
snapPoint
number | string | null–
defaultSnapPoint
number | string | null–
onSnapPointChange
(snapPoint, details) => void–
defaultOpen
booleanfalse
open
boolean–
onOpenChange
(open: boolean, details) => void–
onOpenChangeCompleteCalled after the open or close animation ends.
(open: boolean) => void–
modalThe backdrop only renders when true.
boolean | "trap-focus"true
disablePointerDismissalKeep it open when the backdrop is clicked.
booleanfalse
handle
DrawerHandle<Payload>–
children
ReactNode | ({ payload }) => ReactNode–
PropTypeDefault
handle
DrawerHandle<Payload>–
payload
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AttributeDescription
data-slot="drawer-trigger"Target the trigger in CSS.
data-popup-openPresent while its drawer is open.

Renders the portal, backdrop, viewport and popup, plus the handle. Vertical drawers fit their content up to the viewport height minus 4rem. Side drawers are 75% wide, up to 24rem from the sm breakpoint. Override with h-* or w-*, or scope it to one axis with data-[swipe-axis=y]:.

PropTypeDefault
initialFocus
boolean | RefObject | (type) => HTMLElement | boolean–
finalFocus
boolean | RefObject | (type) => HTMLElement | boolean–
className
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="drawer-popup"The drawer panel.
data-slot="drawer-content"The inner wrapper around your children. It scrolls when nothing else does.
data-swipe-directionup, right, down or left.
data-swipe-axisx or y.
data-openPresent while the drawer is open.
data-starting-stylePresent while it animates in.
data-ending-stylePresent while it animates out.
data-swipingPresent while it is being dragged.
data-snap-pointsPresent when the drawer has snap points.
data-expandedPresent at the full-height snap point.
data-nested-drawer-openPresent while another drawer is open on top.
data-stackPresent while a drawer on the same edge is stacked on top.
--drawer-insetFloats the drawer away from the viewport edges. Defaults to 0px.
--drawer-bleed-backgroundFills the area revealed when dragged past its edge. Defaults to the popover color.
--drawer-swipe-movement-xHorizontal drag distance. A -y variable exists too.
--drawer-snap-point-offsetHow far the current snap point sits below the top.
--nested-drawersHow many drawers are open on top.

Rendered by <DrawerContent /> when modal is true. It fades as you swipe, and stays at least half visible when there are snap points. A drawer layered over a sheet or a drawer on another edge gets a lighter backdrop.

AttributeDescription
data-slot="drawer-overlay"The backdrop.
data-nestedPresent on lighter backdrops of layered drawers.
--drawer-overlay-min-opacityThe lowest opacity it fades to while swiping. 0, or 0.5 with snap points.

Rendered by <DrawerContent /> on the inner edge when showSwipeHandle is on. The whole drawer can be dragged, so the handle is a visual cue.

AttributeDescription
data-slot="drawer-swipe-handle"The handle.

Plain <div> elements that lay out the drawer. The header centers its text in vertical drawers on small screens, the body scrolls and takes the remaining space, and the footer stacks its actions.

AttributeDescription
data-slot="drawer-header"Title and description.
data-slot="drawer-body"Scrollable content.
data-slot="drawer-footer"Actions.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<h2>
AttributeDescription
data-slot="drawer-title"Labels the drawer.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<p>
AttributeDescription
data-slot="drawer-description"Describes the drawer.

Renders no element. Place it inside <Drawer />, around <DrawerContent />. While the software keyboard is open, it adds room below the drawer’s scroll container, scrolls the focused field into view, and makes taps on fields open the keyboard on iOS. Drawers without it are unaffected.

PropTypeDefault
children
ReactNode–
AttributeDescription
--drawer-keyboard-insetSet on the viewport while the keyboard is open: how much of it overlaps the page. Use it with a 0px fallback.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<button>
AttributeDescription
data-slot="drawer-close"Closes the drawer when pressed.

createDrawerHandle<Payload>() returns a handle that connects a <Drawer /> to triggers rendered elsewhere. Create it once, outside your component.