# Sheet

A panel that slides in from an edge of the screen for longer forms and record details.

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

function SheetDemo() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Edit asset
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit asset</SheetTitle>
          <SheetDescription>
            Changes apply to new alerts. Existing alerts keep the old values.
          </SheetDescription>
        </SheetHeader>
        <FieldGroup className="px-4">
          <Field>
            <FieldLabel htmlFor="sheet-asset-name">Hostname</FieldLabel>
            <Input id="sheet-asset-name" defaultValue="srv-db-01.prod" />
          </Field>
          <Field>
            <FieldLabel htmlFor="sheet-asset-ip">IP address</FieldLabel>
            <Input id="sheet-asset-ip" defaultValue="10.20.4.17" />
          </Field>
          <Field>
            <FieldLabel htmlFor="sheet-asset-notes">Notes</FieldLabel>
            <Textarea
              id="sheet-asset-notes"
              defaultValue="Primary database for the billing tenant."
            />
          </Field>
        </FieldGroup>
        <SheetFooter>
          <SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
          <Button>Save changes</Button>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
```

## Usage

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

```tsx
<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>Edit asset</SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Edit asset</SheetTitle>
      <SheetDescription>Changes apply to new alerts.</SheetDescription>
    </SheetHeader>
    <div className="px-4">{/* body */}</div>
    <SheetFooter>
      <SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
      <Button>Save changes</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
```

A sheet is a [Dialog](/components/dialog) docked to an edge of the screen. It behaves like one
(modal, focus trapped, Escape closes it) but has the full height of the viewport, so it fits
content a centered dialog cannot: a form with many fields, the details of a row picked from a
table, a filter panel.

- For a short task with one or two fields, use [Dialog](/components/dialog).
- For a panel the user drags or swipes away on a touch screen, use [Drawer](/components/drawer).
  A sheet has no gestures.
- To confirm a destructive action, use [Alert Dialog](/components/alert-dialog).

Lay out the body as three bands: `SheetHeader`, the body, `SheetFooter`. `SheetContent` spaces
them with a fixed gap. Header and footer carry their own padding; give the body side padding
only (`px-4`). The footer sticks to the bottom edge.

## Examples

### Side

`side` is `right` by default. Use `right` for details and forms, `left` for navigation on small
screens, `top` and `bottom` for short, full-width content such as a notice or a filter bar.

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

function SheetSides() {
  return (
    <>
      {(["top", "right", "bottom", "left"] as const).map((side) => (
        <Sheet key={side}>
          <SheetTrigger render={<Button variant="outline" />}>
            {side}
          </SheetTrigger>
          <SheetContent side={side}>
            <SheetHeader>
              <SheetTitle>Sheet from the {side}</SheetTitle>
              <SheetDescription>
                Top and bottom sheets span the full width. Left and right sheets
                take three quarters of a small screen and their size from the sm
                breakpoint up.
              </SheetDescription>
            </SheetHeader>
            <SheetFooter>
              <SheetClose render={<Button variant="outline" />}>
                Close
              </SheetClose>
            </SheetFooter>
          </SheetContent>
        </Sheet>
      ))}
    </>
  )
}
```

### Sizes

`size` caps the width of left and right sheets from the `sm` breakpoint up: `sm` (25rem, the
default), `md` (35rem), `lg` (50rem) and `xl` (68.75rem). It has no effect on top and bottom
sheets. A plain `className` such as `sm:max-w-3xl` overrides it.

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

function SheetSizes() {
  return (
    <>
      {(["sm", "md", "lg", "xl"] as const).map((size) => (
        <Sheet key={size}>
          <SheetTrigger render={<Button variant="outline" />}>
            Open {size}
          </SheetTrigger>
          <SheetContent size={size}>
            <SheetHeader>
              <SheetTitle>Sheet size {size}</SheetTitle>
              <SheetDescription>
                The size sets the maximum width of left and right sheets.
              </SheetDescription>
            </SheetHeader>
          </SheetContent>
        </Sheet>
      ))}
    </>
  )
}
```

### Scrollable body

Make the body `flex-1 overflow-y-auto` so it scrolls between a fixed header and footer.

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

function SheetScrollable() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Alert history
      </SheetTrigger>
      <SheetContent size="md">
        <SheetHeader>
          <SheetTitle>srv-db-01.prod</SheetTitle>
          <SheetDescription>
            Alerts raised in the last 30 days.
          </SheetDescription>
        </SheetHeader>
        <ul className="flex-1 overflow-y-auto px-4">
          {Array.from({ length: 30 }, (_, index) => (
            <li
              key={index}
              className="flex items-center justify-between gap-4 border-b border-border py-2.5 last:border-0"
            >
              <span>Brute-force login attempt</span>
              <Badge
                variant="soft"
                tone={index % 3 === 0 ? "danger" : "warning"}
              >
                {index % 3 === 0 ? "High" : "Medium"}
              </Badge>
            </li>
          ))}
        </ul>
        <SheetFooter>
          <SheetClose render={<Button variant="outline" />}>Close</SheetClose>
          <Button>Open in alerts</Button>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
```

## Accessibility

- Focus moves into the sheet when it opens and returns to the trigger when it closes. Escape and
  a click on the backdrop close it.
- Always render a `SheetTitle`: it is the sheet's accessible name. Hide it with
  `className="sr-only"` if the design has no visible title.
- `showOverlay={false}` removes the dimmed backdrop but not the modal behavior: the page behind
  stays inert.

## API reference

`import { ... } from "@zenoxai/ui/components/sheet"`

### Sheet

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| actionsRef | `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. |
| defaultOpen | `boolean` | `false` | Whether the dialog is initially open.  To render a controlled dialog, use the `open` prop instead. |
| 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. |
| disablePointerDismissal | `boolean` | `false` | 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. |
| handle | `DialogHandle<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. |
| modal | `boolean \| "trap-focus"` | `true` | 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. |
| onOpenChange | `((open: boolean, eventDetails: DialogRootChangeEventDetails) => 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. |
| open | `boolean` |  | Whether the dialog is currently open. |
| 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). |

### SheetTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| handle | `DialogHandle<unknown>` |  | A handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method. |
| 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` | `true` | 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>`). |
| payload | `unknown` |  | A payload to pass to the dialog when it is opened. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DialogTriggerState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### SheetClose

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| nativeButton | `boolean` | `true` | 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>`). |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DialogCloseState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### SheetContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| finalFocus | `boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| void \| HTMLElement \| null)` |  | 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. |
| initialFocus | `boolean \| RefObject<HTMLElement \| null> \| ((openType: InteractionType) => boolean \| void \| HTMLElement \| null)` |  | 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. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DialogPopupState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
| showCloseButton | `boolean` | `true` | Renders the X button at the top-right corner. Its accessible name is the English "Close": to translate it, turn this off and render your own SheetClose. |
| showOverlay | `boolean` | `true` | Renders the dimmed backdrop. Turning it off does not make the sheet non-modal; set `modal={false}` on Sheet for that. |
| side | `"left" \| "right" \| "top" \| "bottom"` | `right` | Edge the sheet slides in from. Defaults to right. |
| size | `"sm" \| "lg" \| "md" \| "xl"` | `sm` | Maximum width of a left or right sheet from the sm breakpoint: `sm` 400px (default), `md` 560px, `lg` 800px, `xl` 1100px. Below it the sheet is 75% of the viewport wide; top and bottom sheets always span the full width. |

### SheetHeader

No props of its own.

### SheetFooter

No props of its own.

### SheetTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DialogTitleState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### SheetDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, DialogDescriptionState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

