Skip to content
Dethink Components

ComponentsDrawer

Drawer

Open a panel from the edge of the screen.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHandle,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@dethink/components";

Try the examples, then open the code to use them in your app.

Basic

Trigger, sized content, header anatomy, and footer with DrawerClose buttons.

Show sourceexamples/drawer/basic.tsx
examples/drawer/basic.tsx
"use client";

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@dethink/components";
import { ShoppingCart } from "lucide-react";

const items = [
  {
    name: "Dashboard seat",
    detail: "Pro workspace access",
    quantity: "2",
    price: "$48",
  },
  {
    name: "Report export credit",
    detail: "Monthly compliance bundle",
    quantity: "1",
    price: "$19",
  },
];

export function DrawerBasic() {
  return (
    <div className="flex justify-center">
      <Drawer direction="right">
        <DrawerTrigger variant="outline">
          <ShoppingCart aria-hidden="true" className="size-4" />
          Review cart
        </DrawerTrigger>
        <DrawerContent
          dismissible
          showCloseButton
          closeButtonLabel="Close cart"
        >
          <DrawerHeader>
            <DrawerTitle>Your cart</DrawerTitle>
            <DrawerDescription>
              Review the workspace changes before checkout.
            </DrawerDescription>
          </DrawerHeader>
          <div className="text-foreground grid gap-[var(--dt-space-4)] px-[var(--dt-space-6)] py-[var(--dt-space-4)] text-sm">
            <div className="grid gap-[var(--dt-space-3)]">
              {items.map((item) => (
                <div
                  className="border-border/70 bg-muted/30 grid gap-[var(--dt-space-2)] rounded-md border p-[var(--dt-space-3)]"
                  key={item.name}
                >
                  <div className="flex items-start justify-between gap-[var(--dt-space-3)]">
                    <div>
                      <p className="font-medium">{item.name}</p>
                      <p className="text-muted-foreground">{item.detail}</p>
                    </div>
                    <p className="font-semibold">{item.price}</p>
                  </div>
                  <p className="text-muted-foreground text-xs tracking-[0.12em] uppercase">
                    Qty {item.quantity}
                  </p>
                </div>
              ))}
            </div>
            <dl className="border-border/70 grid gap-[var(--dt-space-2)] border-t pt-[var(--dt-space-4)]">
              <div className="flex justify-between">
                <dt className="text-muted-foreground">Subtotal</dt>
                <dd className="font-medium">$67</dd>
              </div>
              <div className="flex justify-between">
                <dt className="text-muted-foreground">Tax estimate</dt>
                <dd className="font-medium">$5</dd>
              </div>
              <div className="flex justify-between text-base">
                <dt className="font-medium">Total</dt>
                <dd className="font-semibold">$72</dd>
              </div>
            </dl>
          </div>
          <DrawerFooter>
            <DrawerClose variant="outline">Cancel</DrawerClose>
            <DrawerClose>Checkout</DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
    </div>
  );
}

Direction-aware sizing

One recipe exercises all four edges plus preset, full-axis, and custom dimension sizing.

Invoice review drawer

Pick an edge and size mode, then open the same drawer content.

Edge

Size

Uses size="lg" on the active edge.

Show sourceexamples/drawer/directional-sizing.tsx
examples/drawer/directional-sizing.tsx
"use client";

import { useState } from "react";
import {
  Button,
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
  type DrawerDirection,
} from "@dethink/components";

const directions = [
  { value: "right", label: "Right", axis: "width" },
  { value: "left", label: "Left", axis: "width" },
  { value: "bottom", label: "Bottom", axis: "height" },
  { value: "top", label: "Top", axis: "height" },
] satisfies Array<{ value: DrawerDirection; label: string; axis: string }>;

const sizeModes = [
  {
    value: "preset",
    label: "Preset lg",
    summary: 'Uses size="lg" on the active edge.',
  },
  {
    value: "custom",
    label: "Custom",
    summary: "Uses 28rem for side drawers or 68dvh for top and bottom drawers.",
  },
  {
    value: "full",
    label: "Full",
    summary: "Fills the viewport on the drawer axis.",
  },
] as const;

type SizeMode = (typeof sizeModes)[number]["value"];

function isVertical(direction: DrawerDirection) {
  return direction === "top" || direction === "bottom";
}

export function DrawerDirectionalSizing() {
  const [direction, setDirection] = useState<DrawerDirection>("right");
  const [sizeMode, setSizeMode] = useState<SizeMode>("preset");
  const customDimension = isVertical(direction) ? "68dvh" : "28rem";
  const activeDirection = directions.find((item) => item.value === direction)!;
  const activeMode = sizeModes.find((item) => item.value === sizeMode)!;

  return (
    <div className="border-border bg-background mx-auto grid w-full max-w-2xl gap-[var(--dt-space-5)] rounded-lg border p-[var(--dt-space-4)]">
      <div className="grid gap-[var(--dt-space-3)] sm:grid-cols-[1fr_auto] sm:items-start">
        <div className="space-y-1">
          <p className="text-foreground text-sm font-medium">
            Invoice review drawer
          </p>
          <p className="text-muted-foreground text-sm leading-6">
            Pick an edge and size mode, then open the same drawer content.
          </p>
        </div>
        <Drawer
          dimension={sizeMode === "custom" ? customDimension : undefined}
          direction={direction}
          fullSize={sizeMode === "full"}
          size="lg"
        >
          <DrawerTrigger>Preview drawer</DrawerTrigger>
          <DrawerContent
            dismissible
            showCloseButton
            closeButtonLabel="Close invoice drawer"
          >
            <DrawerHeader>
              <DrawerTitle>Invoice INV-2048</DrawerTitle>
              <DrawerDescription>
                {activeDirection.label} drawer using{" "}
                {activeMode.label.toLowerCase()}.
              </DrawerDescription>
            </DrawerHeader>
            <div className="grid gap-[var(--dt-space-4)] px-[var(--dt-space-6)] py-[var(--dt-space-4)] text-sm">
              <dl className="grid gap-[var(--dt-space-2)]">
                <div className="flex justify-between gap-[var(--dt-space-4)]">
                  <dt className="text-muted-foreground">Axis</dt>
                  <dd className="text-foreground font-medium">
                    {activeDirection.axis}
                  </dd>
                </div>
                <div className="flex justify-between gap-[var(--dt-space-4)]">
                  <dt className="text-muted-foreground">Mode</dt>
                  <dd className="text-foreground font-medium">
                    {activeMode.label}
                  </dd>
                </div>
                <div className="flex justify-between gap-[var(--dt-space-4)]">
                  <dt className="text-muted-foreground">Custom dimension</dt>
                  <dd className="text-foreground font-medium">
                    {customDimension}
                  </dd>
                </div>
              </dl>
              <div className="border-border/70 bg-muted/30 rounded-md border p-[var(--dt-space-3)]">
                <p className="text-foreground font-medium">Acme procurement</p>
                <p className="text-muted-foreground mt-1">
                  Subscription renewal, usage overage, and support add-on.
                </p>
              </div>
            </div>
            <DrawerFooter>
              <DrawerClose variant="outline">Close</DrawerClose>
              <DrawerClose>Approve invoice</DrawerClose>
            </DrawerFooter>
          </DrawerContent>
        </Drawer>
      </div>

      <div className="grid gap-[var(--dt-space-3)] sm:grid-cols-2">
        <div className="space-y-2">
          <p className="text-muted-foreground text-xs font-semibold tracking-[0.14em] uppercase">
            Edge
          </p>
          <div
            aria-label="Drawer edge"
            className="border-border bg-muted/30 flex flex-wrap gap-[var(--dt-space-1)] rounded-md border p-[var(--dt-space-1)]"
            role="group"
          >
            {directions.map((item) => (
              <Button
                aria-pressed={direction === item.value}
                key={item.value}
                onClick={() => setDirection(item.value)}
                size="sm"
                type="button"
                variant={direction === item.value ? "solid" : "ghost"}
              >
                {item.label}
              </Button>
            ))}
          </div>
        </div>

        <div className="space-y-2">
          <p className="text-muted-foreground text-xs font-semibold tracking-[0.14em] uppercase">
            Size
          </p>
          <div
            aria-label="Drawer size mode"
            className="border-border bg-muted/30 flex flex-wrap gap-[var(--dt-space-1)] rounded-md border p-[var(--dt-space-1)]"
            role="group"
          >
            {sizeModes.map((item) => (
              <Button
                aria-pressed={sizeMode === item.value}
                key={item.value}
                onClick={() => setSizeMode(item.value)}
                size="sm"
                type="button"
                variant={sizeMode === item.value ? "solid" : "ghost"}
              >
                {item.label}
              </Button>
            ))}
          </div>
        </div>
      </div>

      <p className="text-muted-foreground text-sm leading-6">
        {activeMode.summary}
      </p>
    </div>
  );
}

Examples that combine components for common tasks.

Mobile navigation drawer

A left-anchored menu opened via the trigger or an edge swipe from the left.

Show sourceexamples/drawer/mobile-navigation.tsx
examples/drawer/mobile-navigation.tsx
"use client";

import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@dethink/components";
import { Gauge, LayoutDashboard, Settings2, Users } from "lucide-react";

const navLinks = [
  { icon: LayoutDashboard, label: "Overview" },
  { icon: Gauge, label: "Observability" },
  { icon: Users, label: "Team" },
  { icon: Settings2, label: "Settings" },
];

export function DrawerMobileNavigation() {
  return (
    <div className="flex justify-center">
      <Drawer direction="left" edgeSwipeToOpen>
        <DrawerTrigger variant="outline">Open menu</DrawerTrigger>
        <DrawerContent
          dismissible
          closeButtonLabel="Close navigation menu"
          showCloseButton
          size="sm"
        >
          <DrawerHeader>
            <DrawerTitle>Acme Dashboards</DrawerTitle>
            <DrawerDescription>
              Swipe from the left edge, or use the trigger, to open.
            </DrawerDescription>
          </DrawerHeader>
          <nav
            aria-label="Primary"
            className="px-[var(--dt-space-3)] pb-[var(--dt-space-3)]"
          >
            <ul className="grid gap-[var(--dt-space-1)]">
              {navLinks.map(({ icon: Icon, label }) => (
                <li key={label}>
                  <a
                    className="text-foreground hover:bg-muted flex items-center gap-[var(--dt-space-3)] rounded-md px-[var(--dt-space-3)] py-[var(--dt-space-2)] text-sm font-medium"
                    href="#"
                  >
                    <Icon
                      aria-hidden="true"
                      className="text-muted-foreground size-4"
                    />
                    {label}
                  </a>
                </li>
              ))}
            </ul>
          </nav>
        </DrawerContent>
      </Drawer>
    </div>
  );
}

Mobile filter bottom sheet

Snap points (35%, 65%, fully open) with a draggable handle and controlled active snap point.

Show sourceexamples/drawer/filter-bottom-sheet.tsx
examples/drawer/filter-bottom-sheet.tsx
"use client";

import { useState } from "react";
import {
  Checkbox,
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHandle,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
  Field,
  FieldControl,
  FieldLabel,
} from "@dethink/components";

const statuses = ["Open", "In review", "Blocked", "Done"];

export function DrawerFilterBottomSheet() {
  const [activeSnapPoint, setActiveSnapPoint] = useState(0.5);

  return (
    <div className="flex justify-center">
      <Drawer
        activeSnapPoint={activeSnapPoint}
        direction="bottom"
        onActiveSnapPointChange={setActiveSnapPoint}
        snapPoints={[0.35, 0.65, 1]}
      >
        <DrawerTrigger>Filter issues</DrawerTrigger>
        <DrawerContent
          dismissible
          showCloseButton
          closeButtonLabel="Close filters"
        >
          <DrawerHandle aria-label="Drag to resize filters" />
          <DrawerHeader>
            <DrawerTitle>Filters</DrawerTitle>
            <DrawerDescription>
              Drag the handle between 35%, 65%, and fully open, or flick down
              fast to dismiss.
            </DrawerDescription>
          </DrawerHeader>
          <div className="grid gap-[var(--dt-space-3)] px-[var(--dt-space-6)] py-[var(--dt-space-2)]">
            <p className="text-muted-foreground text-xs tracking-wide uppercase">
              Status
            </p>
            {statuses.map((status) => (
              <Field
                id={`status-${status}`}
                key={status}
                orientation="horizontal"
              >
                <FieldControl asChild>
                  <Checkbox
                    defaultChecked={status === "Open"}
                    name="status"
                    value={status}
                  />
                </FieldControl>
                <FieldLabel>{status}</FieldLabel>
              </Field>
            ))}
          </div>
          <DrawerFooter>
            <span className="text-muted-foreground text-sm">
              Snap point: {activeSnapPoint}
            </span>
            <DrawerClose>Apply filters</DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
    </div>
  );
}

Compact Android bottom sheet

A centered 450px-wide bottom sheet with icon-led quick links and light-dismiss.

Show sourceexamples/drawer/android-bottom-sheet.tsx
examples/drawer/android-bottom-sheet.tsx
"use client";

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHandle,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@dethink/components";
import {
  Bell,
  CalendarDays,
  Download,
  LifeBuoy,
  Settings2,
  Share2,
} from "lucide-react";

const quickLinks = [
  {
    description: "Send a secure workspace link",
    href: "#",
    icon: Share2,
    label: "Share report",
  },
  {
    description: "Review meetings and reminders",
    href: "#",
    icon: CalendarDays,
    label: "Open schedule",
  },
  {
    description: "Pull a CSV for this view",
    href: "#",
    icon: Download,
    label: "Export data",
  },
  {
    description: "Tune rules and thresholds",
    href: "#",
    icon: Bell,
    label: "Notification rules",
  },
];

const utilityLinks = [
  { href: "#", icon: Settings2, label: "Settings" },
  { href: "#", icon: LifeBuoy, label: "Support" },
];

export function DrawerAndroidBottomSheet() {
  return (
    <div className="flex justify-center">
      <Drawer direction="bottom" motionPreset="expressive">
        <DrawerTrigger variant="outline">Open compact sheet</DrawerTrigger>
        <DrawerContent
          dismissible
          showCloseButton
          closeButtonLabel="Close quick actions"
          dimension={450}
          className="inset-x-[max(var(--dt-space-4),calc((100vw-450px)/2))] overflow-hidden"
        >
          <DrawerHandle aria-label="Drag to dismiss quick actions" />
          <DrawerHeader>
            <DrawerTitle>Quick actions</DrawerTitle>
            <DrawerDescription>
              A compact 450px Android-style sheet for high-frequency links.
            </DrawerDescription>
          </DrawerHeader>
          <nav
            aria-label="Quick actions"
            className="grid gap-[var(--dt-space-3)] px-[var(--dt-space-3)] py-[var(--dt-space-3)]"
          >
            <ul className="grid gap-[var(--dt-space-1)]">
              {quickLinks.map(({ description, href, icon: Icon, label }) => (
                <li key={label}>
                  <a
                    className="text-foreground hover:bg-muted focus-visible:ring-ring focus-visible:ring-offset-background flex items-center gap-[var(--dt-space-3)] rounded-md px-[var(--dt-space-3)] py-[var(--dt-space-2)] text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
                    href={href}
                  >
                    <span className="border-border/70 bg-muted/60 text-foreground flex size-10 shrink-0 items-center justify-center rounded-md border">
                      <Icon aria-hidden="true" className="size-4" />
                    </span>
                    <span className="min-w-0 flex-1">
                      <span className="block font-medium">{label}</span>
                      <span className="text-muted-foreground block text-xs">
                        {description}
                      </span>
                    </span>
                  </a>
                </li>
              ))}
            </ul>
            <div className="grid grid-cols-2 gap-[var(--dt-space-2)]">
              {utilityLinks.map(({ href, icon: Icon, label }) => (
                <a
                  className="border-border bg-background text-foreground hover:bg-muted focus-visible:ring-ring focus-visible:ring-offset-background flex items-center justify-center gap-[var(--dt-space-2)] rounded-md border px-[var(--dt-space-3)] py-[var(--dt-space-2)] text-sm font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
                  href={href}
                  key={label}
                >
                  <Icon
                    aria-hidden="true"
                    className="text-muted-foreground size-4"
                  />
                  {label}
                </a>
              ))}
            </div>
          </nav>
          <div className="border-border/60 border-t p-[var(--dt-space-3)]">
            <DrawerClose className="w-full" variant="ghost">
              Cancel
            </DrawerClose>
          </div>
        </DrawerContent>
      </Drawer>
    </div>
  );
}

Persistent inspector-rail push panel

modal={false} shifts the record list's layout instead of overlaying it, and never traps focus.

Show sourceexamples/drawer/inspector-rail.tsx
examples/drawer/inspector-rail.tsx
"use client";

import { useState } from "react";
import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@dethink/components";

const records = [
  { id: "REC-104", name: "Northwind renewal", owner: "Priya Shah" },
  { id: "REC-118", name: "Acme onboarding", owner: "Marcus Lee" },
  { id: "REC-129", name: "Globex expansion", owner: "Dana Ruiz" },
];

export function DrawerInspectorRail() {
  const [selected, setSelected] = useState(records[0]!);

  return (
    <div className="border-border flex min-h-[22rem] overflow-hidden rounded-lg border">
      <main className="divide-border flex-1 divide-y overflow-y-auto">
        {records.map((record) => (
          <button
            className="hover:bg-muted data-[current=true]:bg-muted flex w-full items-center justify-between px-[var(--dt-space-4)] py-[var(--dt-space-3)] text-left text-sm"
            data-current={record.id === selected.id}
            key={record.id}
            onClick={() => setSelected(record)}
            type="button"
          >
            <span className="text-foreground font-medium">{record.name}</span>
            <span className="text-muted-foreground">{record.owner}</span>
          </button>
        ))}
      </main>
      <Drawer defaultOpen direction="right" modal={false}>
        <DrawerTrigger
          className="border-border self-start rounded-none border-b"
          variant="ghost"
        >
          Toggle inspector
        </DrawerTrigger>
        <DrawerContent size="sm">
          <DrawerHeader>
            <DrawerTitle>{selected.name}</DrawerTitle>
            <DrawerDescription>
              Push-mode drawers shift layout instead of overlaying it, so the
              record list stays reachable via Tab.
            </DrawerDescription>
          </DrawerHeader>
          <dl className="grid gap-[var(--dt-space-2)] px-[var(--dt-space-6)] py-[var(--dt-space-2)] text-sm">
            <div className="flex justify-between">
              <dt className="text-muted-foreground">ID</dt>
              <dd className="text-foreground font-medium">{selected.id}</dd>
            </div>
            <div className="flex justify-between">
              <dt className="text-muted-foreground">Owner</dt>
              <dd className="text-foreground font-medium">{selected.owner}</dd>
            </div>
          </dl>
        </DrawerContent>
      </Drawer>
    </div>
  );
}

Scheduler event-detail drawer

backgroundScale dims and scales the page behind a modal drawer opened from a calendar grid.

Show sourceexamples/drawer/scheduler-event-detail.tsx
examples/drawer/scheduler-event-detail.tsx
"use client";

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHandle,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
  drawerBackgroundWrapperClassNames,
} from "@dethink/components";
import { Clock, MapPin, Users } from "lucide-react";

export function DrawerSchedulerEventDetail() {
  return (
    <div
      className={drawerBackgroundWrapperClassNames({
        className:
          "border-border bg-background flex justify-center rounded-lg border p-6",
      })}
      data-drawer-background-wrapper=""
    >
      <Drawer backgroundScale direction="right">
        <DrawerTrigger>Open event</DrawerTrigger>
        <DrawerContent
          dismissible
          showCloseButton
          closeButtonLabel="Close event"
        >
          <DrawerHandle aria-label="Drag to dismiss" />
          <DrawerHeader>
            <DrawerTitle>Quarterly planning review</DrawerTitle>
            <DrawerDescription>
              Selected from the scheduler grid. Drag the handle down, or use
              Escape/outside click, to dismiss.
            </DrawerDescription>
          </DrawerHeader>
          <div className="text-foreground grid gap-[var(--dt-space-3)] px-[var(--dt-space-6)] py-[var(--dt-space-2)] text-sm">
            <div className="flex items-center gap-[var(--dt-space-2)]">
              <Clock
                aria-hidden="true"
                className="text-muted-foreground size-4"
              />
              Tue, Mar 10 ยท 10:00โ€“11:00 AM
            </div>
            <div className="flex items-center gap-[var(--dt-space-2)]">
              <MapPin
                aria-hidden="true"
                className="text-muted-foreground size-4"
              />
              Conference room B / video link
            </div>
            <div className="flex items-center gap-[var(--dt-space-2)]">
              <Users
                aria-hidden="true"
                className="text-muted-foreground size-4"
              />
              6 attendees
            </div>
          </div>
          <DrawerFooter>
            <DrawerClose variant="outline">Close</DrawerClose>
            <DrawerClose>Join call</DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </Drawer>
    </div>
  );
}

Nested drill-down edit flow

A drawer opened from inside another drawer automatically recedes its parent.

Show sourceexamples/drawer/nested-drill-down-edit.tsx
examples/drawer/nested-drill-down-edit.tsx
"use client";

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@dethink/components";

export function DrawerNestedDrillDownEdit() {
  return (
    <div className="flex justify-center">
      <Drawer direction="right">
        <DrawerTrigger>Open record</DrawerTrigger>
        <DrawerContent
          dismissible
          showCloseButton
          closeButtonLabel="Close record"
        >
          <DrawerHeader>
            <DrawerTitle>Record</DrawerTitle>
            <DrawerDescription>
              Opening the nested edit drawer recedes this one โ€” the same spring
              primitives as drag-to-dismiss, not a separate animation path.
            </DrawerDescription>
          </DrawerHeader>
          <div className="text-foreground px-[var(--dt-space-6)] py-[var(--dt-space-3)] text-sm">
            Status: <span className="font-medium">In review</span>
          </div>
          <Drawer direction="right">
            <DrawerTrigger className="ms-[var(--dt-space-6)]" variant="outline">
              Edit status
            </DrawerTrigger>
            <DrawerContent
              dismissible
              showCloseButton
              closeButtonLabel="Close status editor"
            >
              <DrawerHeader>
                <DrawerTitle>Edit status</DrawerTitle>
                <DrawerDescription>
                  A drawer opened from inside another drawer.
                </DrawerDescription>
              </DrawerHeader>
              <DrawerFooter>
                <DrawerClose>Done</DrawerClose>
              </DrawerFooter>
            </DrawerContent>
          </Drawer>
        </DrawerContent>
      </Drawer>
    </div>
  );
}

Drawer coordinates the trigger and open state; DrawerContent carries the panel, motion, and dismissal options.

Drawer anatomy
PropWhat it doesDefault
direction"top" | "bottom" | "left" | "right"Anchor edge. A physical anchor โ€” it never repositions under RTL, only internal spacing does."bottom"
modalbooleanfalse renders a non-modal inline push panel that shifts sibling layout instead of overlaying it.true
size / fullSize / dimension"sm" | "md" | "lg" | "xl" | "full" / boolean / string | numberDirection-aware drawer sizing. For top/bottom drawers this controls height; for left/right drawers this controls width. dimension accepts custom CSS lengths, with numbers treated as px."md" / false / โ€”
open / defaultOpen / onOpenChangeboolean / boolean / (open) => voidControlled or uncontrolled open state, identical to Dialog's contract.Not set
DrawerContent โ€” dismissible / keyboardDismissDisabledboolean / booleanOutside-click dismissal when dismissible is true, plus Escape dismissal in modal mode. Requires a visible close affordance when keyboard dismiss is disabled.false / false
snapPoints / activeSnapPoint / defaultSnapPoint / onActiveSnapPointChangenumber[] / number / number / (snapPoint) => voidFractions in (0, 1] of the drawer's open size it can rest at. 0 (closed) is always implicit.Not set
closeThreshold / velocityThresholdnumber / numberDistance fraction and px/s flick velocity that trigger a drag dismiss below the distance threshold.0.25 / 500
dragHandleOnlybooleanRestricts drag initiation to DrawerHandle/header region.true
backgroundScalebooleaniOS-style scale-down/dim on a data-drawer-background-wrapper element while a modal drawer is open.false
edgeSwipeToOpen / edgeSwipeHitRegionSizeboolean / numberOpt-in edge-swipe gesture to open the drawer, and its hit-region size in px.false / 24
motionPreset"none" | "subtle" | "standard" | "expressive"Spring/duration tuning applied consistently to drag, snap, recede, and shared-element morph."standard"
reducedMotionbooleanExplicit override that forces the CSS-only fallback regardless of prefers-reduced-motion.Not set
DrawerContent โ€” layoutIdstringPassthrough to the underlying Motion element for an optional shared-element entrance from a matching motion.* trigger.Not set
DrawerHandledraggable affordancearia-hidden, additive pointer-drag handle. Trigger, close, dismissible outside click, and Escape work without it.Not set
Nested drawersautomaticA drawer opened from inside another drawer automatically recedes its parent using the same spring primitives as drag. Recommended max stack depth: 2.Not set
DrawerHeader / DrawerTitle / DrawerDescription / DrawerFooter / DrawerClosesection componentsAnatomy pieces; the title and description label the drawer for assistive tech.Not set