# Sheet

A sheet slides a panel in from an edge of the window, such as a navigation drawer or a details panel.

A sheet opens a panel against one edge of the window and dims the page behind it. Use it for navigation on a small screen, or for details and edits that belong beside the content people came from. A short task in the middle of the window is a [Dialog](/docs/dialog), and a column that stays open beside the page is part of the [Shell](/docs/shell).


## Usage

```tsx
import { Button, Flex, Sheet, Text } from '@kushagradhawan/kookie-ui';

export function OrderDetails({ order }: { order: { id: string; status: string; total: string } }) {
  return (
    <Sheet.Root>
      <Sheet.Trigger>
        <Button>View order</Button>
      </Sheet.Trigger>
      <Sheet.Content side="end">
        <Flex direction="column" gap="5" height="100%">
          <Flex direction="column" gap="1">
            <Sheet.Title>Order {order.id}</Sheet.Title>
            <Sheet.Description>{order.status}</Sheet.Description>
          </Flex>
          <Text size="2">Total: {order.total}</Text>
          <Flex gap="2" justify="end" mt="auto">
            <Sheet.Close>
              <Button emphasis="quiet" color="gray">Close</Button>
            </Sheet.Close>
            <Button emphasis="loud">Refund order</Button>
          </Flex>
        </Flex>
      </Sheet.Content>
    </Sheet.Root>
  );
}
```

## Anatomy

```tsx
<Sheet.Root>
  <Sheet.Trigger />
  <Sheet.Content>
    <Sheet.Title />
    <Sheet.Description />
    <Sheet.Close />
  </Sheet.Content>
</Sheet.Root>
```

- **Root:** Holds the open state. A sheet is always modal.
- **Trigger:** The element that opens the sheet. It takes one child element, usually a Button.
- **Content:** The panel, rendered in a portal against one edge, with a dimmed overlay behind it.
- **Title:** The sheet's name. It renders a Heading at size 4, medium weight.
- **Description:** A line under the title. It renders Text at size 2 with `emphasis="medium"`.
- **Close:** Closes the sheet when its child is pressed. It takes one child element.

## Best practices

**Open it from the edge people expect.** Navigation comes from the start edge, where the menu button sits. Details and inspectors come from the end edge. A list of actions on a phone comes from the bottom.

**Keep the page it came from in mind.** A sheet is for content that relates to the page behind it. A flow that leaves that page behind belongs on its own page.

**Give it a title, even a hidden one.** The title names the panel for screen readers. Use `Sheet.Title` inside [`VisuallyHidden`](/docs/utilities#visuallyhidden) when the design has no visible heading.

**Put the main action at the end of the panel.** One loud button, with a quiet Close beside it. Two loud buttons in a narrow panel give people no answer to "which one do I press?"

**Don't use a sheet for a quick question.** A confirmation that slides in from the side moves people's eyes away from what they pressed. Use an [Alert Dialog](/docs/alert-dialog).

## Side

`side` sets the edge the panel opens against. `start` is the default.

| Side | Opens against | Use it for |
| --- | --- | --- |
| `start` | The leading edge, left in left-to-right text | Navigation |
| `end` | The trailing edge, right in left-to-right text | Details, inspectors and edit forms |
| `top` | The top edge | Content tied to a top bar, such as search |
| `bottom` | The bottom edge | Actions and pickers on a phone |

`left` and `right` are accepted too. They become `start` and `end`, so the panel follows the reading direction.

## Size

`size` sets the padding and the widest a start or end sheet can be, on the same steps as [Dialog](/docs/dialog): 360, 440, 560 and 720 pixels. Size 2 is the default, and an unset size follows the Theme size.

A start or end sheet fills the window's height and takes 90% of its width up to that cap, so it fills a phone and stops on a desktop. The corners on the sheet's own edge are square, and the two facing the page take the size's corner. Top and bottom sheets span the full width and take 75% of the height. Set `width` or `height` to change these. A `width` above the cap needs `maxWidth` too.

## Examples

### Navigation drawer

Open the app's navigation from a menu button on small screens.

```tsx
<Sheet.Root>
  <Sheet.Trigger>
    <Button iconOnly emphasis="quiet" aria-label="Menu">
      <HugeiconsIcon icon={Menu01Icon} strokeWidth={1.75} />
    </Button>
  </Sheet.Trigger>
  <Sheet.Content side="start" width="280px" aria-label="Navigation">
    <Flex direction="column" gap="1">
      <Button emphasis="quiet" fullWidth asChild>
        <Link href="/projects">Projects</Link>
      </Button>
      <Button emphasis="quiet" fullWidth asChild>
        <Link href="/settings">Settings</Link>
      </Button>
    </Flex>
  </Sheet.Content>
</Sheet.Root>
```

### Responsive width

`width` accepts a responsive object, so a details panel can fill a phone and stay narrow on a desktop.

```tsx
<Sheet.Content side="end" width={{ initial: '100%', md: '400px' }}>
```

### Bottom action sheet

Set `height="auto"` so a bottom sheet is only as tall as its actions.

```tsx
<Sheet.Content side="bottom" height="auto" aria-label="Share">
  <Flex direction="column" gap="2">
    <Button fullWidth>Copy link</Button>
    <Sheet.Close>
      <Button emphasis="quiet" color="gray" fullWidth>Cancel</Button>
    </Sheet.Close>
  </Flex>
</Sheet.Content>
```

### Close after saving

Hold the open state to close the sheet once a request succeeds.

```tsx
const [open, setOpen] = React.useState(false);

<Sheet.Root open={open} onOpenChange={setOpen}>
  {/* trigger and content */}
</Sheet.Root>
```

## Props

### Root

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | | The open state. Controlled |
| `defaultOpen` | `boolean` | `false` | The open state on first render |
| `onOpenChange` | `(open: boolean) => void` | | Called when the sheet opens or closes |

### Content

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `'start' \| 'end' \| 'top' \| 'bottom' \| 'left' \| 'right'` | `'start'` | The edge it opens against |
| `size` | `'1' \| '2' \| '3' \| '4'`, responsive | Theme size | Padding, and the width cap for start and end |
| `width` | `string`, responsive | `90vw` | The panel's width. Start and end only |
| `minWidth` | `string`, responsive | | The narrowest width |
| `maxWidth` | `string`, responsive | The size's width | The widest width. Overrides the size's cap |
| `height` | `string`, responsive | `75vh` | The panel's height. Top and bottom only |
| `minHeight` | `string`, responsive | | The shortest height |
| `maxHeight` | `string`, responsive | | The tallest height |
| `container` | `HTMLElement` | `document.body` | Where the portal renders |
| `forceMount` | `boolean` | | Keeps the sheet in the DOM while closed |
| `onEscapeKeyDown` | `(event) => void` | | Called on Escape. Call `preventDefault()` to keep it open |
| `onPointerDownOutside` | `(event) => void` | | Called on a press outside. Call `preventDefault()` to keep it open |

Content also accepts every attribute of the HTML `div` element.

### Title and Description

Title takes every Heading prop, with `size="4"` and `weight="medium"` by default. Description takes every Text prop, with `size="2"` and `emphasis="medium"` by default.

### Trigger and Close

Both take one child element and pass their behaviour to it. They accept every attribute of the HTML `button` element.

## Accessibility

| Key | Action |
| --- | --- |
| Escape | Closes the sheet |
| Tab | Moves focus to the next control in the sheet |
| Shift + Tab | Moves focus to the previous control in the sheet |

- **Name:** Give the sheet a name with `Sheet.Title` or `aria-label` on Content. In development, a console warning appears when Content has neither.
- **Role:** A screen reader announces a modal dialog. The title names it and the description describes it. Content behind it is hidden from assistive technology.
- **Focus:** Focus moves to the first control in the sheet when it opens, and Tab cycles inside it. Focus returns to the trigger when it closes.
- **Dismissal:** Escape and a press on the overlay both close the sheet.
- **Direction:** `start` and `end` follow the reading direction, so a navigation sheet opens from the right in right-to-left text.
