# Drawer

Open an edge panel with swipe dismissal.

Task filters drawer

[Complete TSX file](/examples/drawer/default.tsx)

```tsx
"use client";

import { ListFilterIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import { Checkbox } from "@/siteplane/components/checkbox";
import { CheckboxGroup } from "@/siteplane/components/checkbox-group";
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";
import { Fieldset, FieldsetLegend } from "@/siteplane/components/fieldset";
import { Label } from "@/siteplane/components/label";
import { Radio, RadioGroup } from "@/siteplane/components/radio-group";
import {
  ToggleGroup,
  ToggleGroupItem,
} from "@/siteplane/components/toggle-group";

const statuses = [
  { count: 18, label: "Active", value: "active" },
  { count: 7, label: "In review", value: "review" },
  { count: 23, label: "Completed", value: "completed" },
];

const priorities = [
  { label: "Low", value: "low" },
  { label: "Medium", value: "medium" },
  { label: "High", value: "high" },
];

const dueDates = [
  { label: "Any time", value: "any" },
  { label: "Next 7 days", value: "week" },
  { label: "This month", value: "month" },
];

export default function Example() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline" />}>
        <ListFilterIcon />
        Filters
      </DrawerTrigger>
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Filters</DrawerTitle>
          <DrawerDescription>
            Narrow down the 48 tasks in Website Redesign.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>
          <div className="grid gap-6">
            <Fieldset>
              <FieldsetLegend>Status</FieldsetLegend>
              <CheckboxGroup defaultValue={["active", "review"]}>
                {statuses.map((status) => (
                  <Label className="w-full" key={status.value}>
                    <Checkbox value={status.value} />
                    {status.label}
                    <span className="ms-auto font-normal text-muted-foreground tabular-nums">
                      {status.count}
                    </span>
                  </Label>
                ))}
              </CheckboxGroup>
            </Fieldset>
            <Fieldset>
              <FieldsetLegend>Priority</FieldsetLegend>
              <ToggleGroup multiple variant="outline" width="full">
                {priorities.map((priority) => (
                  <ToggleGroupItem
                    grow
                    key={priority.value}
                    value={priority.value}
                  >
                    {priority.label}
                  </ToggleGroupItem>
                ))}
              </ToggleGroup>
            </Fieldset>
            <Fieldset>
              <FieldsetLegend>Due date</FieldsetLegend>
              <RadioGroup defaultValue="any">
                {dueDates.map((due) => (
                  <Label key={due.value}>
                    <Radio value={due.value} />
                    {due.label}
                  </Label>
                ))}
              </RadioGroup>
            </Fieldset>
          </div>
        </DrawerPanel>
        <DrawerFooter>
          <DrawerClose render={<Button variant="ghost" />}>Reset</DrawerClose>
          <DrawerClose render={<Button />}>Show 25 tasks</DrawerClose>
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/drawer
```

With npm: `npx shadcn@4.18.0 add @siteplane/drawer`. [Registry JSON](/registry/drawer.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/avatar @siteplane/avatar-group @siteplane/badge @siteplane/button @siteplane/card @siteplane/checkbox-group @siteplane/data-list @siteplane/fieldset @siteplane/file-type-icon @siteplane/label @siteplane/progress @siteplane/radio-group @siteplane/resource-list @siteplane/separator @siteplane/toggle-group
```

## Usage

```tsx
import { Button } from "@/siteplane/components/button";
import {
  Drawer,
  DrawerTrigger,
  DrawerPopup,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerPanel,
  DrawerFooter,
  DrawerClose,
} from "@/siteplane/components/drawer";

<Drawer>
  <DrawerTrigger render={<Button variant="outline" />}>Open</DrawerTrigger>
  <DrawerPopup showBar>
    <DrawerHeader>
      <DrawerTitle>Project details</DrawerTitle>
      <DrawerDescription>Owner, status and due date.</DrawerDescription>
    </DrawerHeader>
    <DrawerPanel>Content</DrawerPanel>
    <DrawerFooter>
      <DrawerClose render={<Button variant="ghost" />}>Close</DrawerClose>
    </DrawerFooter>
  </DrawerPopup>
</Drawer>;
```

## Composition

```text
Drawer
├── DrawerTrigger
└── DrawerPopup
    ├── DrawerHeader
    │   ├── DrawerTitle
    │   └── DrawerDescription
    ├── DrawerPanel
    └── DrawerFooter
        └── DrawerClose
```

## Mobile menu

Open navigation from the left edge on small screens. The header holds the app’s mark and name, the menu groups the destinations and marks the current page with `aria-current="page"`, and the footer pins the account row. `size="xl"` gives the rows touch size: 48 px high, 16 px text and 20 px icons. Keep the items as links and close the drawer when one is chosen.

Mobile navigation drawer

[Complete TSX file](/examples/drawer/mobile-menu.tsx)

```tsx
"use client";

import {
  CalendarIcon,
  ChartColumnIcon,
  ChevronRightIcon,
  FolderKanbanIcon,
  InboxIcon,
  LayoutDashboardIcon,
  type LucideIcon,
  MenuIcon,
  SettingsIcon,
  UsersIcon,
} from "lucide-react";
import { Fragment, useState } from "react";
import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import { Badge } from "@/siteplane/components/badge";
import { Button } from "@/siteplane/components/button";
import {
  Drawer,
  DrawerFooter,
  DrawerHeader,
  DrawerMenu,
  DrawerMenuGroup,
  DrawerMenuGroupLabel,
  DrawerMenuItem,
  DrawerMenuSeparator,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";
import { Separator } from "@/siteplane/components/separator";

type NavigationLink = {
  badge?: string;
  icon: LucideIcon;
  id: string;
  label: string;
};

const groups: { label: string; links: NavigationLink[] }[] = [
  {
    label: "Workspace",
    links: [
      { icon: LayoutDashboardIcon, id: "dashboard", label: "Dashboard" },
      { icon: FolderKanbanIcon, id: "projects", label: "Projects" },
      { badge: "3", icon: InboxIcon, id: "inbox", label: "Inbox" },
      { icon: CalendarIcon, id: "calendar", label: "Calendar" },
    ],
  },
  {
    label: "Manage",
    links: [
      { icon: UsersIcon, id: "team", label: "Team" },
      { icon: ChartColumnIcon, id: "reports", label: "Reports" },
      { icon: SettingsIcon, id: "settings", label: "Settings" },
    ],
  },
];

export default function Example() {
  const [open, setOpen] = useState(false);
  const [page, setPage] = useState("dashboard");

  return (
    <Drawer open={open} onOpenChange={setOpen} position="left">
      <DrawerTrigger render={<Button variant="outline" />}>
        <MenuIcon />
        Menu
      </DrawerTrigger>
      <DrawerPopup showCloseButton>
        <DrawerHeader>
          <div className="flex items-center gap-3">
            <Avatar className="size-8 rounded-lg">
              <AvatarFallback className="rounded-lg">N</AvatarFallback>
            </Avatar>
            <DrawerTitle>Northlight</DrawerTitle>
          </div>
        </DrawerHeader>
        <DrawerPanel>
          <DrawerMenu aria-label="Main" size="xl">
            {groups.map((group, index) => (
              <Fragment key={group.label}>
                {index > 0 && <DrawerMenuSeparator />}
                <DrawerMenuGroup>
                  <DrawerMenuGroupLabel>{group.label}</DrawerMenuGroupLabel>
                  {group.links.map((link) => (
                    <DrawerMenuItem
                      aria-current={page === link.id ? "page" : undefined}
                      key={link.id}
                      onClick={() => {
                        setPage(link.id);
                        setOpen(false);
                      }}
                      render={<a href={`#${link.id}`} />}
                    >
                      <link.icon />
                      {link.label}
                      {link.badge && (
                        <Badge className="ms-auto" profile="count">
                          {link.badge}
                        </Badge>
                      )}
                    </DrawerMenuItem>
                  ))}
                </DrawerMenuGroup>
              </Fragment>
            ))}
          </DrawerMenu>
        </DrawerPanel>
        <Separator />
        <DrawerFooter>
          <DrawerMenu aria-label="Account" className="min-w-0 flex-1" size="xl">
            <DrawerMenuItem
              onClick={() => setOpen(false)}
              render={<a href="#account" />}
            >
              <Avatar>
                <AvatarFallback>ME</AvatarFallback>
              </Avatar>
              <span className="grid min-w-0 flex-1 text-start">
                <span className="truncate">Mara Ellis</span>
                <span className="truncate font-normal text-muted-foreground text-sm">
                  mara@example.com
                </span>
              </span>
              <ChevronRightIcon />
            </DrawerMenuItem>
          </DrawerMenu>
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  );
}
```

## Action menu

Compose the `DrawerMenu` parts into an action sheet, here for a file in a mobile app. Menu items don’t close the drawer on their own, so wrap them in `DrawerClose`; `DrawerMenuTrigger` opens a nested drawer. The parts keep real checkbox, switch and radio semantics; `DrawerMenu` is a navigation region, not an ARIA menu with roving focus. A touch screen turns the rows into 48 px rows; pass `size="xl"` for that size at every pointer.

File actions drawer

[Complete TSX file](/examples/drawer/menu.tsx)

```tsx
"use client";

import {
  CopyIcon,
  DownloadIcon,
  EllipsisIcon,
  FolderIcon,
  FolderInputIcon,
  PencilIcon,
  ShareIcon,
  Trash2Icon,
} from "lucide-react";
import { Button } from "@/siteplane/components/button";
import { Card, CardPanel } from "@/siteplane/components/card";
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerHeader,
  DrawerMenu,
  DrawerMenuCheckboxItem,
  DrawerMenuGroup,
  DrawerMenuGroupLabel,
  DrawerMenuItem,
  DrawerMenuRadioGroup,
  DrawerMenuRadioItem,
  DrawerMenuSeparator,
  DrawerMenuTrigger,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";
import { FileTypeIcon } from "@/siteplane/components/file-type-icon";

const folders = ["Planning", "Marketing", "Archive"];

export default function Example() {
  return (
    <Card className="w-full max-w-md">
      <CardPanel className="flex items-center gap-3">
        <FileTypeIcon name="Q3-launch-plan.pdf" />
        <div className="grid min-w-0 flex-1 text-sm">
          <span className="truncate font-medium">Q3 launch plan.pdf</span>
          <span className="text-muted-foreground">
            2.4 MB · Edited 2 hours ago
          </span>
        </div>
        <Drawer>
          <DrawerTrigger
            render={
              <Button
                aria-label="Actions for Q3 launch plan.pdf"
                shape="square"
                variant="ghost"
              />
            }
          >
            <EllipsisIcon />
          </DrawerTrigger>
          <DrawerPopup showBar>
            <DrawerHeader>
              <div className="flex items-center gap-3">
                <FileTypeIcon name="Q3-launch-plan.pdf" />
                <div className="grid min-w-0 gap-1">
                  <DrawerTitle className="truncate">
                    Q3 launch plan.pdf
                  </DrawerTitle>
                  <DrawerDescription>
                    2.4 MB · Edited 2 hours ago
                  </DrawerDescription>
                </div>
              </div>
            </DrawerHeader>
            <DrawerPanel>
              <DrawerMenu aria-label="File actions">
                <DrawerMenuGroup>
                  <DrawerClose render={<DrawerMenuItem />}>
                    <PencilIcon />
                    Rename
                  </DrawerClose>
                  <DrawerClose render={<DrawerMenuItem />}>
                    <CopyIcon />
                    Duplicate
                  </DrawerClose>
                  <DrawerClose render={<DrawerMenuItem />}>
                    <ShareIcon />
                    Share
                  </DrawerClose>
                  <DrawerClose render={<DrawerMenuItem />}>
                    <DownloadIcon />
                    Download
                  </DrawerClose>
                </DrawerMenuGroup>
                <DrawerMenuSeparator />
                <DrawerMenuCheckboxItem defaultChecked>
                  Pin to top
                </DrawerMenuCheckboxItem>
                <DrawerMenuCheckboxItem variant="switch">
                  Available offline
                </DrawerMenuCheckboxItem>
                <DrawerMenuSeparator />
                <DrawerMenuGroup>
                  <DrawerMenuGroupLabel>Who can open</DrawerMenuGroupLabel>
                  <DrawerMenuRadioGroup
                    aria-label="Who can open"
                    defaultValue="workspace"
                  >
                    <DrawerMenuRadioItem value="invited">
                      Only people invited
                    </DrawerMenuRadioItem>
                    <DrawerMenuRadioItem value="workspace">
                      Anyone in the workspace
                    </DrawerMenuRadioItem>
                    <DrawerMenuRadioItem value="link">
                      Anyone with the link
                    </DrawerMenuRadioItem>
                  </DrawerMenuRadioGroup>
                </DrawerMenuGroup>
                <DrawerMenuSeparator />
                <Drawer>
                  <DrawerMenuTrigger>
                    <FolderInputIcon />
                    Move to
                  </DrawerMenuTrigger>
                  <DrawerPopup showBar>
                    <DrawerHeader>
                      <DrawerTitle>Move to</DrawerTitle>
                    </DrawerHeader>
                    <DrawerPanel>
                      <DrawerMenu aria-label="Folders">
                        {folders.map((folder) => (
                          <DrawerClose key={folder} render={<DrawerMenuItem />}>
                            <FolderIcon />
                            {folder}
                          </DrawerClose>
                        ))}
                      </DrawerMenu>
                    </DrawerPanel>
                  </DrawerPopup>
                </Drawer>
                <DrawerMenuSeparator />
                <DrawerClose render={<DrawerMenuItem variant="destructive" />}>
                  <Trash2Icon />
                  Delete
                </DrawerClose>
              </DrawerMenu>
            </DrawerPanel>
          </DrawerPopup>
        </Drawer>
      </CardPanel>
    </Card>
  );
}
```

## Position

`position` on `Drawer` sets the edge and the matching swipe direction. The popup, bar and swipe area inherit it, so set it on the root. Each edge suits its own content: notifications drop from the top, details open on the right, short confirmations rise from the bottom and switchers slide in from the left. The workspace switcher is a `DrawerMenuRadioGroup` whose rows lead with the workspace's avatar and end with the check (`indicatorPosition="end"`), like the identity rows of [Menu](/docs/components/menu/#checkbox-and-radio-items).

Drawers on each edge

[Complete TSX file](/examples/drawer/positions.tsx)

```tsx
"use client";

import {
  PanelBottomIcon,
  PanelLeftIcon,
  PanelRightIcon,
  PanelTopIcon,
} from "lucide-react";
import type { ReactNode } from "react";
import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import { Badge } from "@/siteplane/components/badge";
import { Button } from "@/siteplane/components/button";
import { Checkbox } from "@/siteplane/components/checkbox";
import { CheckboxGroup } from "@/siteplane/components/checkbox-group";
import {
  DataList,
  DataListItem,
  DataListLabel,
  DataListValue,
} from "@/siteplane/components/data-list";
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerMenu,
  DrawerMenuRadioGroup,
  DrawerMenuRadioItem,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";
import { Fieldset, FieldsetLegend } from "@/siteplane/components/fieldset";
import { Label } from "@/siteplane/components/label";
import {
  ResourceList,
  ResourceListContent,
  ResourceListDescription,
  ResourceListIndicator,
  ResourceListItem,
  ResourceListMeta,
  ResourceListTitle,
} from "@/siteplane/components/resource-list";

const notifications = [
  {
    description: "“The new hero reads much better.”",
    time: "2m",
    title: "Ben Carter commented on Homepage hero",
    tone: "info",
  },
  {
    description: "Due October 30",
    time: "1h",
    title: "Mara Ellis assigned you Pricing table",
    tone: "info",
  },
  {
    description: "Website Redesign",
    time: "Yesterday",
    title: "Priya Shah completed Navigation audit",
    tone: "neutral",
  },
] as const;

const subtasks = [
  { label: "Write the new headline", value: "headline" },
  { label: "Update the illustration", value: "illustration" },
  { label: "Check the mobile layout", value: "mobile" },
];

const workspaces = [
  { initial: "N", name: "Northlight" },
  { initial: "A", name: "Acme Studio" },
  { initial: "P", name: "Polar Labs" },
];

function Detail({ children, label }: { children: ReactNode; label: string }) {
  return (
    <DataListItem>
      <DataListLabel>{label}</DataListLabel>
      <DataListValue>{children}</DataListValue>
    </DataListItem>
  );
}

export default function Example() {
  return (
    <div className="flex flex-wrap gap-2">
      <Drawer position="top">
        <DrawerTrigger render={<Button variant="outline" />}>
          <PanelTopIcon />
          Top
        </DrawerTrigger>
        <DrawerPopup showBar>
          <DrawerHeader>
            <DrawerTitle>Notifications</DrawerTitle>
            <DrawerDescription>
              You have 2 unread notifications.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerPanel>
            <ResourceList>
              {notifications.map((notification) => (
                <ResourceListItem key={notification.title}>
                  <ResourceListIndicator tone={notification.tone} />
                  <ResourceListContent>
                    <ResourceListTitle>{notification.title}</ResourceListTitle>
                    <ResourceListDescription>
                      {notification.description}
                    </ResourceListDescription>
                  </ResourceListContent>
                  <ResourceListMeta>{notification.time}</ResourceListMeta>
                </ResourceListItem>
              ))}
            </ResourceList>
          </DrawerPanel>
          <DrawerFooter>
            <DrawerClose render={<Button variant="ghost" />}>
              Mark all as read
            </DrawerClose>
            <DrawerClose render={<Button />}>Close</DrawerClose>
          </DrawerFooter>
        </DrawerPopup>
      </Drawer>

      <Drawer position="right">
        <DrawerTrigger render={<Button variant="outline" />}>
          <PanelRightIcon />
          Right
        </DrawerTrigger>
        <DrawerPopup>
          <DrawerHeader>
            <div className="flex flex-wrap items-center gap-2">
              <DrawerTitle>Homepage hero</DrawerTitle>
              <Badge variant="info">In review</Badge>
            </div>
            <DrawerDescription>
              Try a shorter headline and a lighter illustration.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerPanel>
            <div className="grid gap-6">
              <DataList className="[--data-list-label-width:6rem]">
                <Detail label="Assignee">
                  <Avatar className="me-2 size-5">
                    <AvatarFallback className="text-[10px]">BC</AvatarFallback>
                  </Avatar>
                  Ben Carter
                </Detail>
                <Detail label="Due date">October 30</Detail>
                <Detail label="Priority">High</Detail>
                <Detail label="Project">Website Redesign</Detail>
              </DataList>
              <Fieldset>
                <FieldsetLegend>Subtasks</FieldsetLegend>
                <CheckboxGroup defaultValue={["headline"]}>
                  {subtasks.map((subtask) => (
                    <Label key={subtask.value}>
                      <Checkbox value={subtask.value} />
                      {subtask.label}
                    </Label>
                  ))}
                </CheckboxGroup>
              </Fieldset>
            </div>
          </DrawerPanel>
          <DrawerFooter>
            <DrawerClose render={<Button variant="ghost" />}>Close</DrawerClose>
            <DrawerClose render={<Button />}>Open task</DrawerClose>
          </DrawerFooter>
        </DrawerPopup>
      </Drawer>

      <Drawer position="bottom">
        <DrawerTrigger render={<Button variant="outline" />}>
          <PanelBottomIcon />
          Bottom
        </DrawerTrigger>
        <DrawerPopup showBar>
          <DrawerHeader>
            <DrawerTitle>Delete Website Redesign?</DrawerTitle>
            <DrawerDescription>
              This permanently deletes the project, its 48 tasks and all
              attachments. You can&apos;t undo this.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <DrawerClose render={<Button variant="ghost" />}>
              Cancel
            </DrawerClose>
            <DrawerClose render={<Button variant="destructive" />}>
              Delete project
            </DrawerClose>
          </DrawerFooter>
        </DrawerPopup>
      </Drawer>

      <Drawer position="left">
        <DrawerTrigger render={<Button variant="outline" />}>
          <PanelLeftIcon />
          Left
        </DrawerTrigger>
        <DrawerPopup>
          <DrawerHeader>
            <DrawerTitle>Workspaces</DrawerTitle>
            <DrawerDescription>Switch to another workspace.</DrawerDescription>
          </DrawerHeader>
          <DrawerPanel>
            <DrawerMenu aria-label="Workspaces">
              <DrawerMenuRadioGroup
                aria-label="Workspace"
                defaultValue="Northlight"
              >
                {workspaces.map((workspace) => (
                  <DrawerMenuRadioItem
                    indicatorPosition="end"
                    key={workspace.name}
                    value={workspace.name}
                  >
                    <Avatar className="size-6 rounded-md">
                      <AvatarFallback className="rounded-md text-[10px]">
                        {workspace.initial}
                      </AvatarFallback>
                    </Avatar>
                    {workspace.name}
                  </DrawerMenuRadioItem>
                ))}
              </DrawerMenuRadioGroup>
            </DrawerMenu>
          </DrawerPanel>
          <DrawerFooter>
            <DrawerClose render={<Button variant="outline" />}>
              Create workspace
            </DrawerClose>
          </DrawerFooter>
        </DrawerPopup>
      </Drawer>
    </div>
  );
}
```

### Width

On wide screens a bottom or top drawer keeps a readable width (`max-w-lg`, 512 px) and centers itself. Set `width="full"` on `DrawerPopup` to let it span the whole edge instead, for a tray with a row of actions. Left and right drawers keep their own width, and a phone shows both widths alike because the screen is narrower than 512 px. The `straight` variant stays attached along the whole edge, so it is full width by default; `width="readable"` narrows it again.

Readable and full-width bottom drawers

[Complete TSX file](/examples/drawer/width.tsx)

```tsx
"use client";

import { PanelBottomIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";

const widths = [
  { label: "Readable", width: "readable" },
  { label: "Full width", width: "full" },
] as const;

export default function Example() {
  return (
    <div className="flex flex-wrap gap-2">
      {widths.map(({ label, width }) => (
        <Drawer key={width} position="bottom">
          <DrawerTrigger render={<Button variant="outline" />}>
            <PanelBottomIcon />
            {label}
          </DrawerTrigger>
          <DrawerPopup showBar width={width}>
            <DrawerHeader>
              <DrawerTitle>Update available</DrawerTitle>
              <DrawerDescription>
                Version 2.4 adds saved views and a faster search. Restart to
                finish the update.
              </DrawerDescription>
            </DrawerHeader>
            <DrawerFooter>
              <DrawerClose render={<Button variant="ghost" />}>
                Later
              </DrawerClose>
              <DrawerClose render={<Button />}>Restart now</DrawerClose>
            </DrawerFooter>
          </DrawerPopup>
        </Drawer>
      ))}
    </div>
  );
}
```

## Variants

`inset` floats the drawer away from the viewport edges on larger screens. `straight` removes the rounded corners and the elevation and keeps the panel attached to its edge, at full width for the bottom and the top (see [Width](#width)).

Default, inset and straight drawers

[Complete TSX file](/examples/drawer/variants.tsx)

```tsx
"use client";

import type { ReactNode } from "react";
import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import { AvatarGroup } from "@/siteplane/components/avatar-group";
import { Badge } from "@/siteplane/components/badge";
import { Button } from "@/siteplane/components/button";
import {
  DataList,
  DataListItem,
  DataListLabel,
  DataListValue,
} from "@/siteplane/components/data-list";
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";
import {
  Progress,
  ProgressIndicator,
  ProgressLabel,
  ProgressTrack,
  ProgressValue,
} from "@/siteplane/components/progress";

const team = ["BC", "ME", "PS"];

const variants = [
  { variant: "default", label: "Default" },
  { variant: "inset", label: "Inset" },
  { variant: "straight", label: "Straight" },
] as const;

function Detail({ children, label }: { children: ReactNode; label: string }) {
  return (
    <DataListItem>
      <DataListLabel>{label}</DataListLabel>
      <DataListValue>{children}</DataListValue>
    </DataListItem>
  );
}

export default function Example() {
  return (
    <div className="flex flex-wrap gap-2">
      {variants.map(({ variant, label }) => (
        <Drawer key={variant} position="right">
          <DrawerTrigger render={<Button variant="outline" />}>
            {label}
          </DrawerTrigger>
          <DrawerPopup variant={variant}>
            <DrawerHeader>
              <div className="flex flex-wrap items-center gap-2">
                <DrawerTitle>Website Redesign</DrawerTitle>
                <Badge variant="success">On track</Badge>
              </div>
              <DrawerDescription>
                Refresh the marketing site and the design system docs.
              </DrawerDescription>
            </DrawerHeader>
            <DrawerPanel>
              <div className="grid gap-6">
                <DataList className="[--data-list-label-width:6rem]">
                  <Detail label="Owner">
                    <Avatar className="me-2 size-5">
                      <AvatarFallback className="text-[10px]">
                        BC
                      </AvatarFallback>
                    </Avatar>
                    Ben Carter
                  </Detail>
                  <Detail label="Team">
                    <AvatarGroup size="sm">
                      {team.map((initials) => (
                        <Avatar key={initials}>
                          <AvatarFallback className="text-[10px]">
                            {initials}
                          </AvatarFallback>
                        </Avatar>
                      ))}
                    </AvatarGroup>
                  </Detail>
                  <Detail label="Due date">October 30</Detail>
                  <Detail label="Budget">€24,000</Detail>
                </DataList>
                <Progress value={64}>
                  <div className="flex justify-between gap-4">
                    <ProgressLabel>Progress</ProgressLabel>
                    <ProgressValue />
                  </div>
                  <ProgressTrack>
                    <ProgressIndicator />
                  </ProgressTrack>
                </Progress>
              </div>
            </DrawerPanel>
            <DrawerFooter>
              <DrawerClose render={<Button variant="ghost" />}>
                Close
              </DrawerClose>
              <DrawerClose render={<Button />}>Open project</DrawerClose>
            </DrawerFooter>
          </DrawerPopup>
        </Drawer>
      ))}
    </div>
  );
}
```

## Scrollable content

`DrawerPanel` scrolls when the content is taller than the drawer; the header and footer stay in place.

Scrollable activity drawer

[Complete TSX file](/examples/drawer/scrolling.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Drawer,
  DrawerClose,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";

const activity = [
  { time: "9:41 AM", text: "Ben Carter moved “Homepage hero” to In review." },
  { time: "9:32 AM", text: "Mara Ellis commented on “Pricing table”." },
  { time: "9:05 AM", text: "Sam Ortiz uploaded 3 files to Brand assets." },
  {
    time: "8:47 AM",
    text: "Ben Carter assigned “Footer links” to Priya Shah.",
  },
  { time: "8:30 AM", text: "Priya Shah completed “Navigation audit”." },
  { time: "Yesterday", text: "Mara Ellis changed the due date to October 30." },
  { time: "Yesterday", text: "Sam Ortiz created “Case study layout”." },
  {
    time: "Yesterday",
    text: "Ben Carter renamed the project to Website Redesign.",
  },
  { time: "Monday", text: "Priya Shah invited Sofia Nguyen to the project." },
  { time: "Monday", text: "Mara Ellis completed “Content inventory”." },
  { time: "Monday", text: "Sam Ortiz commented on “Homepage hero”." },
  { time: "Last week", text: "Sofia Nguyen uploaded the new logo files." },
  { time: "Last week", text: "Priya Shah created “Accessibility review”." },
  {
    time: "Last week",
    text: "Mara Ellis moved “Pricing table” to In progress.",
  },
  { time: "Last week", text: "Sam Ortiz commented on “Brand assets”." },
  { time: "Last week", text: "Ben Carter added the project brief." },
  { time: "2 weeks ago", text: "Ben Carter created the project." },
];

export default function Example() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant="outline" />}>
        View activity
      </DrawerTrigger>
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Activity</DrawerTitle>
          <DrawerDescription>
            Recent changes in Website Redesign.
          </DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>
          <ul className="grid gap-4">
            {activity.map((item) => (
              <li key={item.text} className="grid gap-0.5 text-sm">
                <p>{item.text}</p>
                <p className="text-muted-foreground text-xs">{item.time}</p>
              </li>
            ))}
          </ul>
        </DrawerPanel>
        <DrawerFooter>
          <DrawerClose render={<Button />}>Close</DrawerClose>
        </DrawerFooter>
      </DrawerPopup>
    </Drawer>
  );
}
```

## Snap points

Pass `snapPoints` to rest a bottom drawer at a compact height before it expands to full height. Numbers between 0 and 1 are fractions of the viewport height; strings accept `px` and `rem`. Control the active point with `snapPoint` and `onSnapPointChange`.

Comments drawer with snap points

[Complete TSX file](/examples/drawer/snap-points.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Drawer,
  DrawerDescription,
  DrawerHeader,
  DrawerPanel,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from "@/siteplane/components/drawer";

const snapPoints = ["300px", 1];

const comments = [
  {
    author: "Ben Carter",
    time: "2 hours ago",
    text: "The new hero reads much better. Can we try a shorter headline?",
  },
  {
    author: "Mara Ellis",
    time: "2 hours ago",
    text: "Agreed. I'll send two options this afternoon.",
  },
  {
    author: "Priya Shah",
    time: "1 hour ago",
    text: "Let's keep the pricing link visible on small screens.",
  },
  {
    author: "Sam Ortiz",
    time: "1 hour ago",
    text: "The illustration export is ready in Brand assets.",
  },
  {
    author: "Ben Carter",
    time: "45 minutes ago",
    text: "Thanks! Moving this to review once the copy is final.",
  },
  {
    author: "Sofia Nguyen",
    time: "30 minutes ago",
    text: "Could we check contrast on the secondary button?",
  },
  {
    author: "Mara Ellis",
    time: "20 minutes ago",
    text: "Headline options are in the doc. I prefer the second one.",
  },
  {
    author: "Priya Shah",
    time: "10 minutes ago",
    text: "Second one works for me too.",
  },
];

export default function Example() {
  return (
    <Drawer snapPoints={snapPoints}>
      <DrawerTrigger render={<Button variant="outline" />}>
        View comments
      </DrawerTrigger>
      <DrawerPopup showBar>
        <DrawerHeader>
          <DrawerTitle>Comments</DrawerTitle>
          <DrawerDescription>Homepage hero · 8 comments</DrawerDescription>
        </DrawerHeader>
        <DrawerPanel>
          <ul className="grid gap-4">
            {comments.map((comment) => (
              <li key={comment.text} className="grid gap-1 text-sm">
                <p>
                  <span className="font-medium">{comment.author}</span>{" "}
                  <span className="text-muted-foreground text-xs">
                    {comment.time}
                  </span>
                </p>
                <p>{comment.text}</p>
              </li>
            ))}
          </ul>
        </DrawerPanel>
      </DrawerPopup>
    </Drawer>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| Drawer.position | bottom \| top \| left \| right | bottom | Shared edge and inferred swipe direction. |
| Drawer.swipeDirection | down \| up \| left \| right | derived from position | Override swipe direction. |
| DrawerPopup.position / DrawerBar.position / DrawerSwipeArea.position | bottom \| top \| left \| right | root position | Local edge override. |
| DrawerPopup.variant / DrawerViewport.variant | default \| straight \| inset | default | Surface and viewport layout. |
| DrawerPopup.width | readable \| full | readable; full for straight | Width of a bottom or top panel on a wide screen. readable caps it at 512 px (max-w-lg) and centers it; full spans the whole edge. Left and right panels keep their own width. |
| DrawerPopup.elevation | default \| none | default; none for straight | default draws the floating shadow and its edge; none removes both. A straight drawer always has none and draws a hairline along its inner edge instead. |
| DrawerPopup.showCloseButton / showBar | boolean | false | Optional automatic close control or drag bar. |
| DrawerPopup.closeProps | DrawerPrimitive.Close.Props | unset | Overrides the automatic close control, including aria-label (default: Close). |
| DrawerPopup.portalProps | DrawerPrimitive.Portal.Props | unset | Portal configuration. |
| DrawerViewport.position | bottom \| top \| left \| right | unset | Low-level viewport; Popup supplies resolved position. |
| DrawerHeader.allowSelection | boolean | false | Allow text selection instead of header dragging. |
| DrawerFooter.allowSelection / DrawerPanel.allowSelection | boolean | true | Allow text selection. |
| DrawerPanel.scrollFade / scrollable | boolean | true | Scroll fade and scroll-area rendering. |
| DrawerMenuItem.variant | default \| destructive | default | Action tone. A destructive item colors its label and its icon red. |
| DrawerMenuItem.disabled | boolean | false | Prevents action. |
| DrawerMenu.size | sm \| md \| lg \| xl | provider / lg | Row size. xl is the touch size: 48 px rows with 16 px text and 20 px icons. The other sizes keep the compact menu rows, which a touch screen also turns into 48 px rows. |
| DrawerMenuCheckboxItem.variant | default \| switch | default | Checkbox or switch presentation. |
| DrawerMenuRadioItem.value | string | required | Selection value. |
| DrawerMenuRadioItem.indicatorPosition | start \| end | start | Where the check shows: in the leading slot, or at the end of an identity row that leads with a logo or an avatar. |

DrawerPopup includes Portal, Backdrop and Viewport automatically. DrawerContent is an alias of DrawerPopup, as in the other overlays. DrawerPortal is a direct foundation export. DrawerTrigger, Close, SwipeArea, Title and Description wrap [Base UI Drawer](https://base-ui.com/react/components/drawer). DrawerBackdrop/Viewport are available for low-level composition. DrawerBar is the visual drag handle. DrawerCreateHandle creates detached-trigger handles; DrawerPrimitive exposes the namespace. DrawerPosition is not exported as a named type.

Header, Footer, Panel, Bar, MenuItem, MenuSeparator, MenuGroup and MenuGroupLabel accept useRender div/button props according to their element; DrawerMenu renders nav. DrawerMenuTrigger accepts Trigger props except render and supplies its own Button/chevron. DrawerMenuCheckboxItem accepts Checkbox props and becomes a Switch when `variant` is `switch`. DrawerMenuRadioGroup accepts RadioGroup props, and DrawerMenuRadioItem accepts Radio props with a string `value`. The choice parts style themselves as menu rows. Plain menu actions do not close the drawer automatically. All styled className props are strings. Foundation snap points, nesting, focus, modal state, dismissal and events remain available on their corresponding root/parts.

`open`/`defaultOpen` choose controlled/initial state (closed by default); `onOpenChange(open, eventDetails)` requests a transition and `onOpenChangeComplete(open)` reports motion completion. Initial/final focus and dismissal behavior belong to the underlying foundation.

`DrawerMenuSeparator`, `DrawerMenuGroup`, and `DrawerMenuGroupLabel` accept useRender div props. They supply the menu divider, group layout, and label styling respectively, without selection state or roving focus of their own.

[Read the installed source](/source/drawer).

## Usage notes

Provide a title, clear close action and meaningful focus target. Escape and focus restoration follow the foundation. Application loading, errors and async confirmation must be represented explicitly in the content. Do not treat swipe dismissal as the only way to close.
