# Sheet

A panel that slides in from an edge of the window for a task or detail that belongs beside the page.

- **Import:** `import { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from "@booleanpress/ui/sheet"`
- **Radix Dialog:** <https://www.radix-ui.com/primitives/docs/components/dialog>
- **APG Dialog (Modal):** <https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/>
- **Page:** <https://ui.booleanpress.com/components/sheet> · @booleanpress/ui 0.1.0

## Usage

A sheet is a modal dialog drawn against an edge. Use it for a form or a detail view that should keep the page visible behind it; use a [dialog](/components/dialog) for a short, centred task. `SheetTitle` names it and `SheetDescription` describes it.

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

export function EditConnection() {
  return (
    <Sheet>
      <SheetTrigger asChild>
        <Button variant="outline">Edit connection</Button>
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit connection</SheetTitle>
          <SheetDescription>Change how this mailer signs in.</SheetDescription>
        </SheetHeader>
        <SheetFooter>
          <SheetClose asChild>
            <Button>Save</Button>
          </SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
```

`side` chooses the edge: `right` (the default), `left`, `top` or `bottom`. Left and right sheets are three quarters of the window wide, at most 384 px. The sheet is a flex column: give the part that should scroll `flex-1 overflow-y-auto`, and the header and footer stay in place. `showCloseButton={false}` removes the × when the footer offers the ways out. Control it with `open` and `onOpenChange`; without a trigger, focus returns to the element that had it, or to `returnFocusTo`.

## Examples

### Basic

A form in a right-hand sheet, with Save and Cancel in the footer.

```tsx
import { Button } from "@booleanpress/ui/button"
import { Input } from "@booleanpress/ui/input"
import { Label } from "@booleanpress/ui/label"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@booleanpress/ui/sheet"

export default function SheetBasic() {
  return (
    <Sheet>
      <SheetTrigger asChild>
        <Button variant="outline">Edit connection</Button>
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit connection</SheetTitle>
          <SheetDescription>Change how this mailer signs in to the mail server.</SheetDescription>
        </SheetHeader>
        <div className="flex flex-col gap-2 px-4">
          <Label htmlFor="sheet-host">Server</Label>
          <Input id="sheet-host" defaultValue="smtp.example.com" />
          <Label htmlFor="sheet-port" className="mt-2">
            Port
          </Label>
          <Input id="sheet-port" defaultValue="587" inputMode="numeric" />
        </div>
        <SheetFooter>
          <SheetClose asChild>
            <Button>Save</Button>
          </SheetClose>
          <SheetClose asChild>
            <Button variant="outline">Cancel</Button>
          </SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
```

### Sides

`side` on each of the four edges.

```tsx
import { Button } from "@booleanpress/ui/button"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@booleanpress/ui/sheet"

const SIDES = ["top", "right", "bottom", "left"] as const

export default function SheetSides() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      {SIDES.map((side) => (
        <Sheet key={side}>
          <SheetTrigger asChild>
            <Button variant="outline" className="capitalize">
              {side}
            </Button>
          </SheetTrigger>
          <SheetContent side={side}>
            <SheetHeader>
              <SheetTitle className="capitalize">{side} sheet</SheetTitle>
              <SheetDescription>It slides in from the {side} edge of the window.</SheetDescription>
            </SheetHeader>
          </SheetContent>
        </Sheet>
      ))}
    </div>
  )
}
```

### Long content

A list taller than the window scrolls; the header and footer stay visible.

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

const EVENTS = Array.from({ length: 30 }, (_, i) => `Delivery attempt ${i + 1}: accepted by the mail server`)

export default function SheetLongContent() {
  return (
    <Sheet>
      <SheetTrigger asChild>
        <Button variant="outline">Open the delivery history</Button>
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Delivery history</SheetTitle>
          <SheetDescription>Every attempt for this email, newest last.</SheetDescription>
        </SheetHeader>
        <ol className="flex-1 overflow-y-auto px-4 text-sm">
          {EVENTS.map((event) => (
            <li key={event} className="border-b py-2">
              {event}
            </li>
          ))}
        </ol>
        <SheetFooter>
          <SheetClose asChild>
            <Button variant="outline">Close history</Button>
          </SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
```

### Without the ×

`showCloseButton={false}`: the footer and Escape are the ways out.

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

export default function SheetNoCloseButton() {
  return (
    <Sheet>
      <SheetTrigger asChild>
        <Button variant="outline">Review the changes</Button>
      </SheetTrigger>
      <SheetContent showCloseButton={false}>
        <SheetHeader>
          <SheetTitle>Review the changes</SheetTitle>
          <SheetDescription>The × is off here: the footer holds the only ways out, and Escape also closes it.</SheetDescription>
        </SheetHeader>
        <SheetFooter>
          <SheetClose asChild>
            <Button>Apply</Button>
          </SheetClose>
          <SheetClose asChild>
            <Button variant="outline">Discard</Button>
          </SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
```

## Accessibility

**Semantics.** A `div` with `role="dialog"` and `aria-modal="true"`, named by `SheetTitle` and described by `SheetDescription`. The rest of the page is hidden from assistive technology while it is open.

**Labels.** Every sheet needs a `SheetTitle`; keep it for screen readers with `className="sr-only"` if the design shows none. The × button's name comes from the provider's `close` string.

**Focus.** Focus moves into the sheet when it opens, Tab and Shift+Tab stay inside, and focus returns to the trigger when it closes. Without a trigger it returns to the element that had focus when it opened, or to `returnFocusTo` when that element is gone.

**Known limits.**

- One modal at a time: a sheet can open a select or a popover, not a second sheet or dialog.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Escape | Closes the sheet. |
| Tab | Moves to the next focusable element inside the sheet, wrapping from the last to the first. |
| Shift + Tab | Moves to the previous focusable element, wrapping from the first to the last. |

## API

### Sheet

Renders Radix Sheet.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` |  | Whether it starts open, when it controls itself. |
| `modal` | `boolean` |  | Keep it modal: the rest of the page cannot be reached while it is open. Leave it on. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether it is open, when you control it. Pair it with `onOpenChange`. |

### SheetTrigger

Renders Radix Sheet.Trigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### SheetClose

Renders Radix Sheet.Close and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### SheetContent

Renders Radix Sheet.Content and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `deferPointerDownOutside` | `boolean` |  | When `true`, a `'pointerdown'` event outside of the layered element will wait for the interaction's click event before dispatching, allowing third-party code to stop propagation of later events and cancel dismissal. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Event handler called when the escape key is down. Can be prevented. |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
| `onOpenAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on open. Can be prevented. |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
| `returnFocusTo` | `ReturnFocusTarget` |  | Where focus goes on close when the element that opened the overlay no longer exists — for example after the confirmed action deleted the row it sat in. |
| `showCloseButton` | `boolean` | `true` | Render the × button. |
| `side` | `"left" \| "right" \| "top" \| "bottom"` | `right` | The screen edge it slides from. |

### SheetHeader

Renders a `div` and passes it every other prop.

### SheetFooter

Renders a `div` and passes it every other prop.

### SheetTitle

Renders Radix Sheet.Title and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### SheetDescription

Renders Radix Sheet.Description and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="sheet"` (Sheet), `data-slot="sheet-trigger"` (SheetTrigger), `data-slot="sheet-close"` (SheetClose), `data-slot="sheet-content"` (SheetContent), `data-slot="sheet-header"` (SheetHeader), `data-slot="sheet-footer"` (SheetFooter), `data-slot="sheet-title"` (SheetTitle), `data-slot="sheet-description"` (SheetDescription).

**Provider strings:** `close` (`BooleanUIProvider`'s `strings`).

## Theming

The panel is a floating surface, like menus and popovers: `--popover` and `--popover-foreground`. The left and right sheets are placed with logical edges (`start`, `end`), so in a right-to-left page `side="right"` sits at the left edge; the slide animation still moves along the physical axis.

| Token | Used for |
| --- | --- |
| `--background` | background |
| `--foreground` | text |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
