# Sheet

A floating panel that slides in from an edge of the screen.

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

```tsx
import { Button } from "@/components/ui/button";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";

export default function SheetBasic() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="secondary" />}>Edit task</SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Update pricing page</SheetTitle>
          <SheetDescription>Website redesign · Task 14</SheetDescription>
        </SheetHeader>
        <SheetBody>
          <FieldGroup className="gap-3">
            <Field>
              <FieldLabel htmlFor="task-title">Title</FieldLabel>
              <Input id="task-title" defaultValue="Update pricing page" />
            </Field>
            <Field>
              <FieldLabel htmlFor="task-notes">Notes</FieldLabel>
              <Textarea
                id="task-notes"
                defaultValue="Match the new plan names and add the comparison table."
              />
            </Field>
          </FieldGroup>
        </SheetBody>
        <SheetFooter>
          <SheetClose render={<Button variant="secondary" />}>Cancel</SheetClose>
          <SheetClose render={<Button variant="primary" />}>Save</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  );
}
```

## 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/sheet
```

## Usage

```tsx
import { Button } from "@/components/ui/button";
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";

export function FiltersSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="secondary" />}>Filters</SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Filters</SheetTitle>
          <SheetDescription>Narrow down the task list.</SheetDescription>
        </SheetHeader>
        <SheetBody>…</SheetBody>
        <SheetFooter>
          <SheetClose render={<Button variant="primary" />}>Apply</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  );
}
```

A sheet is a [Dialog](https://ui.jarv.is/components/dialog.md) shaped as a side panel. Always include a `SheetTitle`.

## Composition

- `Sheet`
  - `SheetTrigger`
  - `SheetContent`
    - `SheetHeader`
      - `SheetTitle`
      - `SheetDescription`
    - `SheetBody`
    - `SheetFooter`
      - `SheetClose`

## Examples

### Scrolling body

`SheetBody` scrolls between a fixed header and footer.

```tsx
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";

const activity = [
  { who: "Sam Rivera", initials: "SR", what: "moved this task to In progress", when: "2h ago" },
  { who: "Ada Park", initials: "AP", what: "attached homepage-v2.fig", when: "5h ago" },
  {
    who: "Priya Shah",
    initials: "PS",
    what: "commented: “Let’s ship the mobile layout first.”",
    when: "Yesterday",
  },
  { who: "Jonah Kim", initials: "JK", what: "changed the due date to Mar 28", when: "Yesterday" },
  { who: "Sam Rivera", initials: "SR", what: "added the Pricing label", when: "Mar 14" },
  {
    who: "Lena Ortiz",
    initials: "LO",
    what: "linked this task to the launch checklist",
    when: "Mar 13",
  },
  { who: "Marco Ruiz", initials: "MR", what: "assigned this task to Sam Rivera", when: "Mar 12" },
  { who: "Ada Park", initials: "AP", what: "renamed this task", when: "Mar 12" },
  { who: "Priya Shah", initials: "PS", what: "added the Design label", when: "Mar 11" },
  { who: "Jonah Kim", initials: "JK", what: "commented: “Copy is final.”", when: "Mar 11" },
  { who: "Lena Ortiz", initials: "LO", what: "attached pricing-copy.docx", when: "Mar 10" },
  { who: "Sam Rivera", initials: "SR", what: "set the priority to High", when: "Mar 9" },
  { who: "Marco Ruiz", initials: "MR", what: "moved this task to Up next", when: "Mar 8" },
  { who: "Ada Park", initials: "AP", what: "added a checklist with 4 items", when: "Mar 8" },
  { who: "Priya Shah", initials: "PS", what: "set the due date to Mar 21", when: "Mar 7" },
  { who: "Jonah Kim", initials: "JK", what: "added Lena Ortiz as a watcher", when: "Mar 6" },
  { who: "Lena Ortiz", initials: "LO", what: "linked the brand guidelines", when: "Mar 5" },
  { who: "Sam Rivera", initials: "SR", what: "added the Website label", when: "Mar 4" },
  { who: "Ada Park", initials: "AP", what: "created this task", when: "Mar 3" },
];

export default function SheetScrolling() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="secondary" />}>View activity</SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Activity</SheetTitle>
          <SheetDescription>Update pricing page · Task 14</SheetDescription>
        </SheetHeader>
        <SheetBody className="gap-3">
          {activity.map((entry, index) => (
            <div key={index} className="flex gap-3 text-sm">
              <Avatar size="sm" className="mt-0.5">
                <AvatarFallback>{entry.initials}</AvatarFallback>
              </Avatar>
              <p className="min-w-0 flex-1 text-muted-foreground">
                <span className="font-medium text-foreground">{entry.who}</span> {entry.what}
                <span className="block text-xs text-faint">{entry.when}</span>
              </p>
            </div>
          ))}
        </SheetBody>
        <SheetFooter>
          <span className="mr-auto flex items-center gap-2 text-2xs font-medium text-muted-foreground">
            <Kbd size="sm">Esc</Kbd>
            Close
          </span>
          <SheetClose render={<Button variant="secondary" />}>Done</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  );
}
```

### Sides

`side` sets the edge the sheet slides in from.

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

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";

const sides = ["top", "right", "bottom", "left"] as const;

export default function SheetSides() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {sides.map((side) => (
        <Sheet key={side}>
          <SheetTrigger render={<Button variant="secondary" className="capitalize" />}>
            {side}
          </SheetTrigger>
          <SheetContent side={side}>
            <SheetHeader>
              <SheetTitle>Share “Mobile app”</SheetTitle>
              <SheetDescription>Invite people or copy a link.</SheetDescription>
            </SheetHeader>
            <SheetBody className="gap-3 sm:flex-row">
              <Input
                placeholder="name@example.com"
                aria-label="Email address"
                className="sm:max-w-sm"
              />
              <Button variant="secondary">
                <LinkIcon />
                Copy link
              </Button>
            </SheetBody>
          </SheetContent>
        </Sheet>
      ))}
    </div>
  );
}
```

## Props

| Part | Extends |
| --- | --- |
| `Sheet` | [`Dialog.Root`](https://base-ui.com/react/components/dialog#root) |
| `SheetTrigger` | [`Dialog.Trigger`](https://base-ui.com/react/components/dialog#trigger) |
| `SheetClose` | [`Dialog.Close`](https://base-ui.com/react/components/dialog#close) |
| `SheetContent` | [`Dialog.Popup`](https://base-ui.com/react/components/dialog#popup) |
| `SheetHeader` | `<div>` |
| `SheetBody` | `<div>` |
| `SheetFooter` | `<div>` |
| `SheetTitle` | [`Dialog.Title`](https://base-ui.com/react/components/dialog#title) |
| `SheetDescription` | [`Dialog.Description`](https://base-ui.com/react/components/dialog#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.

### Sheet

| Prop                       | Type                                                                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| -------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open?`                    | `boolean`                                                                | Whether the dialog is currently open.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `defaultOpen?`             | `boolean`                                                                | Whether the dialog is initially open. To render a controlled dialog, use the `open` prop instead. Default: `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `modal?`                   | `"trap-focus" \| boolean`                                                | Determines if the dialog enters a modal state when open. - `true`: user interaction is limited to just the dialog: 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 dialog, but document page scroll is not locked and pointer interactions outside of it remain enabled. When `modal` is `true` or `'trap-focus'`, render `<Dialog.Close>` inside `<Dialog.Popup>` so touch screen readers can escape the popup. Default: `true` |
| `onOpenChange?`            | `((open: boolean, eventDetails: Sheet.Root.ChangeEventDetails) => void)` | Event handler called when the dialog is opened or closed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `onOpenChangeComplete?`    | `((open: boolean) => void)`                                              | Event handler called after any animations complete when the dialog is opened or closed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `disablePointerDismissal?` | `boolean`                                                                | Whether to prevent the dialog from closing on outside presses. For non-modal dialogs, this also prevents the dialog from closing when focus moves outside of it. Default: `false`                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `actionsRef?`              | `React.RefObject<DialogRootActions \| null>`                             | A ref to imperative actions. - `unmount`: Ends the closing phase of the dialog after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the dialog completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the dialog imperatively when called.                                                                                                                                                                                                                                  |
| `handle?`                  | `Sheet.Handle<unknown>`                                                  | A handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open state. Can be created with the Dialog.createHandle() method.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `children?`                | `PayloadChildRenderFunction<unknown> \| React.ReactNode`                 | The content of the dialog. This can be a regular React node or a render function that receives the `payload` of the active trigger.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `triggerId?`               | `string \| null`                                                         | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a controlled dialog. There's no need to specify this prop when the dialog is uncontrolled (that is, when the `open` prop is not set).                                                                                                                                                                                                                                                                                                                                                          |
| `defaultTriggerId?`        | `string \| null`                                                         | ID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open dialog.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

### SheetTrigger

| Prop            | Type                    | Description                                                                                                                                                                                        |
| --------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `handle?`       | `Sheet.Handle<unknown>` | A handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method.                                                                                             |
| `payload?`      | `unknown`               | A payload to pass to the dialog 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 the dialog in controlled mode (with the DialogRoot `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`  |

### SheetClose

| 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` |

### SheetContent

| Prop               | Type                                                                                                                          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `initialFocus?`    | `boolean \| React.RefObject<HTMLElement \| null> \| ((openType: InteractionType) => boolean \| HTMLElement \| null \| void)`  | Determines the element to focus when the dialog is opened. By default, focus moves to the first tabbable element inside the popup, except when the dialog is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard. - `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, `null` to fall back to 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 dialog 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, `null` to fall back to the default behavior, or `false`/`undefined` to do nothing.                                                                                                                                                                                       |
| `side?`            | `"bottom" \| "left" \| "right" \| "top"`                                                                                      | Default: `"right"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `showCloseButton?` | `boolean`                                                                                                                     | Default: `true`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
