ComponentsDrawer
Drawer
Open a panel from the edge of the screen.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- Motion for React
(motion)Animations, transitions, and reduced-motion handling.
- React Aria
(react-aria)Accessibility hooks and overlay utilities used by the implementation.
- React Aria Components
(react-aria-components)Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
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
"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 min-h-0 flex-1 content-start gap-[var(--dt-space-4)] overflow-y-auto overscroll-contain 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 variant="solid">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
"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.
Current view: Overview
Mobile filter bottom sheet
Drag or use the size buttons to resize. Filters scroll independently while Apply stays reachable at every snap point.
Showing: Open
Show sourceexamples/drawer/filter-bottom-sheet.tsx
"use client";
import { useState } from "react";
import {
Checkbox,
Button,
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.65);
const [selected, setSelected] = useState(["Open"]);
const [applied, setApplied] = useState(["Open"]);
return (
<div className="flex flex-col items-center gap-3">
<p className="text-muted-foreground text-sm" role="status">
Showing: {applied.length ? applied.join(", ") : "all statuses"}
</p>
<Drawer
activeSnapPoint={activeSnapPoint}
direction="bottom"
onActiveSnapPointChange={setActiveSnapPoint}
snapPoints={[0.35, 0.65, 1]}
size="xl"
>
<DrawerTrigger>Filter issues</DrawerTrigger>
<DrawerContent
dismissible
showCloseButton
closeButtonLabel="Close filters"
>
<DrawerHandle aria-label="Drag to resize filters" />
<DrawerHeader className="py-3">
<DrawerTitle>Filters</DrawerTitle>
<DrawerDescription className="sr-only">
Choose statuses, then apply your filters. Use the sheet size
controls to resize.
</DrawerDescription>
</DrawerHeader>
<div className="grid min-h-0 flex-1 content-start gap-3 overflow-y-auto overscroll-contain px-6 py-3">
<div
role="group"
aria-label="Sheet size"
className="flex flex-wrap gap-2 motion-reduce:hidden"
>
{(
[
[0.35, "Compact"],
[0.65, "Medium"],
[1, "Expanded"],
] as const
).map(([point, label]) => (
<Button
key={point}
size="sm"
variant={activeSnapPoint === point ? "solid" : "outline"}
aria-pressed={activeSnapPoint === point}
onClick={() => setActiveSnapPoint(point)}
>
{label}
</Button>
))}
</div>
<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
checked={selected.includes(status)}
onCheckedChange={(checked) =>
setSelected((current) =>
checked
? [...current, status]
: current.filter((value) => value !== status),
)
}
name="status"
value={status}
/>
</FieldControl>
<FieldLabel>{status}</FieldLabel>
</Field>
))}
</div>
<DrawerFooter className="flex-row items-center justify-between px-6 py-3">
<span className="text-muted-foreground text-sm">
{selected.length} selected
</span>
<DrawerClose variant="solid" onPress={() => setApplied(selected)}>
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
"use client";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
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="min(36rem, 90dvh)"
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>Shortcuts for your workspace.</DrawerDescription>
</DrawerHeader>
<nav
aria-label="Quick actions"
className="grid min-h-0 flex-1 content-start gap-[var(--dt-space-3)] overflow-y-auto overscroll-contain 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>
<DrawerFooter>
<DrawerClose className="w-full" variant="ghost">
Cancel
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</div>
);
}Persistent inspector-rail push panel
modal={false} shifts the record list's layout instead of overlaying it, and never traps focus.
Northwind renewal
Push-mode drawers shift layout instead of overlaying it, so the record list stays reachable via Tab.
- ID
- REC-104
- Owner
- Priya Shah
Show sourceexamples/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
"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
Edit and save a record status in a nested drawer, or go Back without saving. The parent remains visible behind the editor.
Show sourceexamples/drawer/nested-drill-down-edit.tsx
"use client";
import { useState } from "react";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
RadioGroup,
RadioGroupItem,
} from "@dethink/components";
export function DrawerNestedDrillDownEdit() {
const [status, setStatus] = useState("In review");
const [draft, setDraft] = useState(status);
return (
<div className="flex justify-center">
<Drawer direction="right">
<DrawerTrigger>Open record</DrawerTrigger>
<DrawerContent
dismissible
showCloseButton
closeButtonLabel="Close record"
>
<DrawerHeader>
<DrawerTitle>Record</DrawerTitle>
<DrawerDescription>
Review this record and update its progress.
</DrawerDescription>
</DrawerHeader>
<div
className="text-foreground px-[var(--dt-space-6)] py-[var(--dt-space-3)] text-sm"
role="status"
>
Status: <span className="font-medium">{status}</span>
</div>
<Drawer direction="right">
<DrawerTrigger
className="mx-[var(--dt-space-6)] self-start"
variant="outline"
onPress={() => setDraft(status)}
>
Edit status
</DrawerTrigger>
<DrawerContent
dismissible
showCloseButton
closeButtonLabel="Close status editor"
>
<DrawerHeader>
<DrawerTitle>Edit status</DrawerTitle>
<DrawerDescription>
Choose the next stage for this record.
</DrawerDescription>
</DrawerHeader>
<div className="min-h-0 flex-1 overflow-y-auto p-6">
<RadioGroup
aria-label="Record status"
value={draft}
onValueChange={setDraft}
className="grid gap-3"
>
{["Open", "In review", "Blocked", "Done"].map((value) => (
<label
key={value}
className="border-border has-[:checked]:border-primary has-[:checked]:bg-primary/5 flex cursor-pointer items-center gap-3 rounded-lg border p-4"
>
<RadioGroupItem value={value} />
{value}
</label>
))}
</RadioGroup>
</div>
<DrawerFooter>
<DrawerClose variant="outline">Back</DrawerClose>
<DrawerClose variant="solid" onPress={() => setStatus(draft)}>
Save changes
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
</DrawerContent>
</Drawer>
</div>
);
}Drawer coordinates the trigger and open state; DrawerContent carries the panel, motion, and dismissal options.
Snap points are fractions of the configured drawer size. Initialize a controlled snap point to one of the supplied stops. For a fixed header and footer, put scrolling content in a min-h-0 flex-1 overflow-y-auto body. Provide buttons alongside drag resizing. With reduced motion, the panel opens at its full configured size without drag or spring animation, keeping every control accessible.
| Prop | What it does | Default |
|---|---|---|
direction"top" | "bottom" | "left" | "right" | Anchor edge. A physical anchor — it never repositions under RTL, only internal spacing does. | "bottom" |
modalboolean | false 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 | number | Direction-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) => void | Controlled or uncontrolled open state, identical to Dialog's contract. | Not set |
DrawerContent — dismissible / keyboardDismissDisabledboolean / boolean | Outside-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) => void | Fractions in (0, 1] of the drawer's open size it can rest at. 0 (closed) is always implicit. | Not set |
closeThreshold / velocityThresholdnumber / number | Distance fraction and px/s flick velocity that trigger a drag dismiss below the distance threshold. | 0.25 / 500 |
dragHandleOnlyboolean | Restricts drag initiation to DrawerHandle/header region. | true |
backgroundScaleboolean | iOS-style scale-down/dim on a data-drawer-background-wrapper element while a modal drawer is open. | false |
edgeSwipeToOpen / edgeSwipeHitRegionSizeboolean / number | Opt-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" |
reducedMotionboolean | Explicit override that forces the CSS-only fallback regardless of prefers-reduced-motion. | Not set |
DrawerContent — layoutIdstring | Passthrough to the underlying Motion element for an optional shared-element entrance from a matching motion.* trigger. | Not set |
DrawerHandledraggable affordance | aria-hidden, additive pointer-drag handle. Trigger, close, dismissible outside click, and Escape work without it. | Not set |
Nested drawersautomatic | A 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 components | Anatomy pieces; the title and description label the drawer for assistive tech. | Not set |