# Drawer

A panel that slides in from an edge and can be swiped away.

Source: [drawer.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/drawer.tsx)

```tsx
import { MinusIcon, PlusIcon } from "lucide-react";
import * as React from "react";

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

export default function DrawerBasic() {
  const [goal, setGoal] = React.useState(5);
  return (
    <Drawer showSwipeHandle>
      <DrawerTrigger render={<Button variant="secondary" />}>Set a goal</DrawerTrigger>
      <DrawerContent>
        <div className="mx-auto flex w-full max-w-sm flex-col">
          <DrawerHeader>
            <DrawerTitle>Daily focus goal</DrawerTitle>
            <DrawerDescription>How many tasks do you want to finish each day?</DrawerDescription>
          </DrawerHeader>
          <div className="flex items-center justify-center gap-6 p-6">
            <Button
              variant="secondary"
              size="icon-lg"
              className="rounded-full"
              aria-label="Decrease"
              disabled={goal <= 1}
              focusableWhenDisabled
              onClick={() => setGoal((goal) => goal - 1)}
            >
              <MinusIcon />
            </Button>
            <div className="flex w-24 flex-col items-center">
              <span className="text-5xl font-medium tabular-nums">{goal}</span>
              <span className="text-2xs text-faint">tasks per day</span>
            </div>
            <Button
              variant="secondary"
              size="icon-lg"
              className="rounded-full"
              aria-label="Increase"
              disabled={goal >= 20}
              focusableWhenDisabled
              onClick={() => setGoal((goal) => goal + 1)}
            >
              <PlusIcon />
            </Button>
          </div>
          <DrawerFooter>
            <DrawerClose render={<Button variant="primary" />}>Set goal</DrawerClose>
            <DrawerClose render={<Button variant="secondary" />}>Cancel</DrawerClose>
          </DrawerFooter>
        </div>
      </DrawerContent>
    </Drawer>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/drawer
```

## Usage

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

export function ShareDrawer() {
  return (
    <Drawer showSwipeHandle>
      <DrawerTrigger render={<Button variant="secondary" />}>Share</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Share project</DrawerTitle>
          <DrawerDescription>Invite people or copy a link.</DrawerDescription>
        </DrawerHeader>
        <div className="p-4">…</div>
        <DrawerFooter>
          <DrawerClose render={<Button variant="secondary" />}>Close</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
}
```

A drawer slides up from the bottom and can be swiped away, which suits phones. For a panel without
swiping, use a [Sheet](https://ui.jarv.is/components/sheet.md).

`showSwipeHandle` adds a grip. Keep a close button too, and always include a `DrawerTitle`.

## Composition

- `Drawer`
  - `DrawerTrigger`
  - `DrawerContent`
    - `DrawerHeader`
      - `DrawerTitle`
      - `DrawerDescription`
    - `DrawerFooter`
      - `DrawerClose`

## Examples

### Swipe direction

`swipeDirection` sets which way the drawer is swiped to close: `down`, `up`, `left` or `right`.

```tsx
import { BellIcon } from "lucide-react";

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

const notifications = [
  { title: "Sam Rivera mentioned you", detail: "in Update pricing page", time: "2m" },
  { title: "Invoice INV-1042 was paid", detail: "Acme Corp", time: "1h" },
  { title: "Deployment ready", detail: "website-redesign", time: "3h" },
];

export default function DrawerSwipeDirection() {
  return (
    <Drawer swipeDirection="right">
      <DrawerTrigger render={<Button variant="secondary" />}>Notifications</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Notifications</DrawerTitle>
          <DrawerDescription>Swipe right to dismiss.</DrawerDescription>
        </DrawerHeader>
        <div className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2">
          {notifications.map(({ title, detail, time }) => (
            <div key={title} className="flex gap-3 rounded-md px-2 py-2 hover:bg-hover">
              <BellIcon className="mt-0.5 size-4 shrink-0 text-icon-muted" />
              <div className="min-w-0 flex-1">
                <p className="text-sm font-medium">{title}</p>
                <p className="text-xs text-faint">{detail}</p>
              </div>
              <span className="text-xs text-faint">{time}</span>
            </div>
          ))}
        </div>
        <DrawerFooter>
          <DrawerClose render={<Button variant="secondary" />}>Close</DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  );
}
```

## Props

| Part | Extends |
| --- | --- |
| `Drawer` | [`Drawer.Root`](https://base-ui.com/react/components/drawer#root) |
| `DrawerTrigger` | [`Drawer.Trigger`](https://base-ui.com/react/components/drawer#trigger) |
| `DrawerPortal` | [`Drawer.Portal`](https://base-ui.com/react/components/drawer#portal) |
| `DrawerClose` | [`Drawer.Close`](https://base-ui.com/react/components/drawer#close) |
| `DrawerOverlay` | [`Drawer.Backdrop`](https://base-ui.com/react/components/drawer#backdrop) |
| `DrawerSwipeHandle` | `<div>` |
| `DrawerContent` | [`Drawer.Popup`](https://base-ui.com/react/components/drawer#popup) |
| `DrawerHeader` | `<div>` |
| `DrawerFooter` | `<div>` |
| `DrawerTitle` | [`Drawer.Title`](https://base-ui.com/react/components/drawer#title) |
| `DrawerDescription` | [`Drawer.Description`](https://base-ui.com/react/components/drawer#description) |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### Drawer

| Prop                       | Type                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| -------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open?`                    | `boolean`                                                                                               | Whether the drawer is currently open.                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `defaultOpen?`             | `boolean`                                                                                               | Whether the drawer is initially open. To render a controlled drawer, use the `open` prop instead. Default: `false`                                                                                                                                                                                                                                                                                                                                                        |
| `modal?`                   | `"trap-focus" \| boolean`                                                                               | Determines if the drawer enters a modal state when open. - `true`: user interaction is limited to just the drawer: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the drawer, but document page scroll is not locked and pointer interactions outside of it remain enabled. Default: `true` |
| `onOpenChange?`            | `((open: boolean, eventDetails: Drawer.Root.ChangeEventDetails) => void)`                               | Event handler called when the drawer is opened or closed.                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `onOpenChangeComplete?`    | `((open: boolean) => void)`                                                                             | Event handler called after any animations complete when the drawer is opened or closed.                                                                                                                                                                                                                                                                                                                                                                                   |
| `disablePointerDismissal?` | `boolean`                                                                                               | Whether to prevent the drawer from closing on outside presses. For non-modal drawers, this also prevents the drawer from closing when focus moves outside of it. Default: `false`                                                                                                                                                                                                                                                                                         |
| `actionsRef?`              | `React.RefObject<DrawerRootActions \| null>`                                                            | A ref to imperative actions. - `unmount`: Ends the closing phase of the drawer after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the drawer completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the drawer imperatively when called.                                                                                          |
| `handle?`                  | `Drawer.Handle<unknown>`                                                                                | A handle to associate the drawer with a trigger. If specified, allows detached triggers to control the drawer's open state. Can be created with the Drawer.createHandle() method.                                                                                                                                                                                                                                                                                         |
| `triggerId?`               | `string \| null`                                                                                        | ID of the trigger that the drawer is associated with. This is useful in conjunction with the `open` prop to create a controlled drawer. There's no need to specify this prop when the drawer is uncontrolled (that is, when the `open` prop is not set).                                                                                                                                                                                                                  |
| `defaultTriggerId?`        | `string \| null`                                                                                        | ID of the trigger that the drawer is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open drawer.                                                                                                                                                                                                                                                                                                                       |
| `children?`                | `PayloadChildRenderFunction<unknown> \| React.ReactNode`                                                | The content of the drawer.                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `swipeDirection?`          | `"down" \| "left" \| "right" \| "up"`                                                                   | The swipe direction used to dismiss the drawer. Default: `"down"`                                                                                                                                                                                                                                                                                                                                                                                                         |
| `snapPoints?`              | `DrawerSnapPoint[]`                                                                                     | Snap points used to position the drawer. Use numbers between 0 and 1 to represent fractions of the viewport height, numbers greater than 1 as pixel values, or strings in `px`/`rem` units (for example, `'148px'` or `'30rem'`).                                                                                                                                                                                                                                         |
| `snapToSequentialPoints?`  | `boolean`                                                                                               | Disables velocity-based snap skipping so drag distance determines the next snap point. Default: `false`                                                                                                                                                                                                                                                                                                                                                                   |
| `snapPoint?`               | `DrawerSnapPoint \| null`                                                                               | The currently active snap point. Use with `onSnapPointChange` to control the snap point.                                                                                                                                                                                                                                                                                                                                                                                  |
| `defaultSnapPoint?`        | `DrawerSnapPoint \| null`                                                                               | The initial snap point value when uncontrolled.                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `onSnapPointChange?`       | `((snapPoint: DrawerSnapPoint \| null, eventDetails: Drawer.Root.SnapPointChangeEventDetails) => void)` | Callback fired when the snap point changes.                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `showSwipeHandle?`         | `boolean`                                                                                               | Default: `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                          |

### DrawerTrigger

| Prop            | Type                     | Description                                                                                                                                                                                       |
| --------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `handle?`       | `Drawer.Handle<unknown>` | A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method.                                                                                            |
| `payload?`      | `unknown`                | A payload to pass to the drawer when it is opened.                                                                                                                                                |
| `id?`           | `string`                 | ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for drawers in controlled mode (with the Drawer.Root `triggerId` prop).  |
| `nativeButton?` | `boolean`                | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Default: `true` |

### DrawerPortal

| Prop           | Type                                                                                      | Description                                                                               |
| -------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `keepMounted?` | `boolean`                                                                                 | Whether to keep the portal mounted in the DOM while the popup is hidden. Default: `false` |
| `container?`   | `HTMLElement \| React.RefObject<HTMLElement \| ShadowRoot \| null> \| ShadowRoot \| null` | A parent element to render the portal element into.                                       |

### DrawerClose

| Prop            | Type      | Description                                                                                                                                                                                       |
| --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nativeButton?` | `boolean` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Default: `true` |

### DrawerOverlay

| Prop           | Type      | Description                                                                 |
| -------------- | --------- | --------------------------------------------------------------------------- |
| `forceRender?` | `boolean` | Whether the backdrop is forced to render even when nested. Default: `false` |

### DrawerContent

| Prop            | Type                                                                                                                          | Description                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `initialFocus?` | `boolean \| React.RefObject<HTMLElement \| null> \| ((openType: InteractionType) => boolean \| HTMLElement \| null \| void)`  | Determines the element to focus when the drawer is opened. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (first tabbable element or popup). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.       |
| `finalFocus?`   | `boolean \| React.RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| HTMLElement \| null \| void)` | Determines the element to focus when the drawer is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing. |
