---
title: Sheet
description: Present a focused drawer from a viewport edge.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Sheet

> Present a focused drawer from a viewport edge.

Use Sheet for a focused drawer from a viewport edge, such as mobile navigation or an inspector. It shares modal dismissal and focus behavior with its overlay family.

## Basic usage

```tsx
import { Button, Sheet } from 'moraine'

export function Example() {
  return (
    <Sheet>
      <Sheet.Trigger as={Button}>Open</Sheet.Trigger>
      <Sheet.Content title="Details">
        <Sheet.Body>Sheet content</Sheet.Body>
        <Sheet.Footer>
          <Sheet.Close as={Button}>Done</Sheet.Close>
        </Sheet.Footer>
      </Sheet.Content>
    </Sheet>
  )
}
```

## Anatomy

```text
Sheet [component; no DOM]
├── Sheet.Trigger [part; slot=trigger]
├── Sheet.Close [part]
├── overlay [slot]
└── Sheet.Content [part; slot=content]
    ├── Sheet.Header [part; slot=header]
    │   ├── Sheet.Title [part; slot=title]
    │   ├── Sheet.Description [part; slot=description]
    │   └── Sheet.Action [part; slot=action]
    ├── contentClose [slot]
    ├── Sheet.Body [part; slot=body]
    └── Sheet.Footer [part; slot=footer]
```

Content is portaled. Overlay and Content are siblings by default; scrollable overlay mode nests Content inside Overlay.

## Usage

### Content composition

Use Content `title` and `description` for simple cases:

```tsx
<Sheet>
  <Sheet.Content title="Settings" description="Update your account.">
    <Sheet.Body>...</Sheet.Body>
    <Sheet.Footer>...</Sheet.Footer>
  </Sheet.Content>
</Sheet>
```

For a custom header layout, compose the anatomy parts:

```tsx
<Sheet.Content>
  <Sheet.Header>
    <Sheet.Title>Settings</Sheet.Title>
    <Sheet.Description>Update your account.</Sheet.Description>
    <Sheet.Action>...</Sheet.Action>
  </Sheet.Header>
  <Sheet.Body>...</Sheet.Body>
  <Sheet.Footer>...</Sheet.Footer>
</Sheet.Content>
```

An explicit `Sheet.Header` replaces the header generated by Content's `title` and `description`.
`Sheet.Title` and `Sheet.Description` connect their IDs to the surface's accessible name and description.
Without a visible title, supply root `ariaLabel` or native ARIA labeling on Content.

`Sheet.Action` places actions beside the title. `Sheet.Close` is an explicit close control you can
place inside the root; it is separate from Content's automatic corner close button.
Use root `close={false}` to hide the automatic close button, or `closeIcon` to replace its icon.

### Focus and outside interaction

`modal` defaults to `true`: focus stays inside the surface, outside content is hidden from assistive
technology, and native outside pointer actions are prevented. Closing restores focus to the trigger.

Setting `modal={false}` allows focus to leave the surface, but does not disable Escape or outside
dismissal. `dismissible` controls those close requests. Scroll locking is independent:
`preventScroll` defaults to `true`. For interaction with the background page, also set
`preventScroll={false}` and `overlay={false}`.

### Styling and portal placement

Apply `class` and object `style` directly to each rendered part. Root `classes` and `styles` set
slot defaults for the family; Content's slot maps cover only `overlay`, `content`, and `contentClose`.
See [Customization](https://moraine.subf.dev/docs/customization.md#the-4-layer-override-hierarchy) for precedence.

Set root `portalMount` to place Content in a chosen container. This also works for controlled
surfaces without a Trigger.

### Structured drawer

Choose the viewport edge with root `side`, and use `inset` when the drawer needs space around it.
Compose body and footer regions to keep the task content separate from its actions.

```tsx
import { Button, Sheet } from 'moraine'

export function DrawerUsage() {
  return (
    <Sheet>
      <Sheet.Trigger as={Button}>Open Settings Drawer</Sheet.Trigger>
      <Sheet.Content title="Settings drawer" description="A drawer with body and footer regions.">
        <Sheet.Body>
          <div class="text-xs text-muted-foreground py-4">
            Place settings fields in this region and actions in the footer.
          </div>
        </Sheet.Body>
        <Sheet.Footer>
          <div class="flex gap-2 w-full justify-end">
            <Sheet.Close as={Button} variant="ghost">
              Cancel
            </Sheet.Close>
            <Sheet.Close as={Button}>Done</Sheet.Close>
          </div>
        </Sheet.Footer>
      </Sheet.Content>
    </Sheet>
  )
}
```

### State and dismissal

Use controlled open state when a layout owns visibility. Disable normal dismissal only when the sheet has a clear explicit close action; `onClosePrevent` receives blocked outside or Escape attempts.

```tsx
import { Button, Sheet } from 'moraine'
import { createSignal } from 'solid-js'

export function StateUsage() {
  const [open, setOpen] = createSignal(false)

  return (
    <div>
      <Button onClick={() => setOpen(true)}>Open Managed Sheet</Button>
      <Sheet open={open()} onOpenChange={setOpen}>
        <Sheet.Content
          title="Controlled Sheet"
          description="Controlled open state enables external workflow triggers."
        >
          <Sheet.Body>
            <p class="text-xs text-muted-foreground py-2">
              Reactive state is managed by parent container.
            </p>
          </Sheet.Body>
          <Sheet.Footer>
            <div class="flex w-full justify-end">
              <Button size="xs" onClick={() => setOpen(false)}>
                Done
              </Button>
            </div>
          </Sheet.Footer>
        </Sheet.Content>
      </Sheet>
    </div>
  )
}
```

### Keyboard interaction

| Key                    | Description                                       |
| ---------------------- | ------------------------------------------------- |
| <kbd>Escape</kbd>      | Requests dismissal when `dismissible={true}`.     |
| <kbd>Tab</kbd>         | Moves focus forward; modal sheets contain focus.  |
| <kbd>Shift + Tab</kbd> | Moves focus backward; modal sheets contain focus. |

## Examples

### Dismiss control

```tsx
import { Button, Sheet } from 'moraine'
import { createSignal } from 'solid-js'

export function DismissControl() {
  const [open, setOpen] = createSignal(false)
  const [preventedCloseCount, setPreventedCloseCount] = createSignal(0)

  return (
    <div class="flex flex-wrap gap-3 items-center">
      <Sheet
        open={open()}
        onOpenChange={setOpen}
        dismissible={false}
        onClosePrevent={() => setPreventedCloseCount((value) => value + 1)}
      >
        <Sheet.Trigger as={Button} variant="outline">
          Open persistent sheet
        </Sheet.Trigger>
        <Sheet.Content
          title="Persistent sheet"
          description="Outside click and Escape key dismissal are blocked."
        >
          <Sheet.Body>
            <div class="py-2 space-y-3">
              <p class="text-sm text-muted-foreground">
                This sheet cannot be dismissed by clicking the overlay or pressing Escape.
              </p>
              <p class="text-sm text-foreground">
                Prevented close attempts: <span class="font-medium">{preventedCloseCount()}</span>
              </p>
            </div>
          </Sheet.Body>
          <Sheet.Footer>
            <div class="flex w-full justify-end">
              <Button size="sm" onClick={() => setOpen(false)}>
                Close sheet
              </Button>
            </div>
          </Sheet.Footer>
        </Sheet.Content>
      </Sheet>
    </div>
  )
}
```

### Sides

```tsx
import { Button, Sheet } from 'moraine'

export function Sides() {
  return (
    <div class="flex flex-wrap gap-3 items-center">
      <Sheet side="left">
        <Sheet.Trigger as={Button} variant="outline" size="sm" leading="i-lucide:panel-left">
          Left (Navigation)
        </Sheet.Trigger>
        <Sheet.Content title="Application Navigation" description="Jump to any workspace section.">
          <Sheet.Body>
            <div class="py-2 space-y-2">
              <Button
                variant="ghost"
                class="w-full justify-start"
                leading="i-lucide:layout-dashboard"
              >
                Dashboard
              </Button>
              <Button
                variant="ghost"
                class="w-full justify-start"
                leading="i-lucide:git-pull-request"
              >
                Pull Requests
              </Button>
              <Button variant="ghost" class="w-full justify-start" leading="i-lucide:server">
                Deployments
              </Button>
              <Button variant="ghost" class="w-full justify-start" leading="i-lucide:settings">
                Settings
              </Button>
            </div>
          </Sheet.Body>
        </Sheet.Content>
      </Sheet>

      <Sheet side="right">
        <Sheet.Trigger as={Button} variant="outline" size="sm" leading="i-lucide:shopping-cart">
          Right (Cart Drawer)
        </Sheet.Trigger>
        <Sheet.Content
          title="Shopping Cart (2 items)"
          description="Review your selected items before checkout."
        >
          <Sheet.Body>
            <div class="text-xs py-2 space-y-3">
              <div class="p-2 rounded-lg bg-muted/40 flex items-center justify-between">
                <div>
                  <p class="font-medium">Canvas backpack</p>
                  <p class="text-muted-foreground">Qty: 1</p>
                </div>
                <span class="font-mono font-semibold">$199.00</span>
              </div>
              <div class="p-2 rounded-lg bg-muted/40 flex items-center justify-between">
                <div>
                  <p class="font-medium">Travel organizer</p>
                  <p class="text-muted-foreground">Qty: 1</p>
                </div>
                <span class="font-mono font-semibold">$49.00</span>
              </div>
            </div>
          </Sheet.Body>
          <Sheet.Footer>
            <Button class="w-full" variant="default">
              Proceed to Checkout ($248.00)
            </Button>
          </Sheet.Footer>
        </Sheet.Content>
      </Sheet>

      <Sheet side="bottom">
        <Sheet.Trigger as={Button} variant="outline" size="sm" leading="i-lucide:share-2">
          Bottom (Share)
        </Sheet.Trigger>
        <Sheet.Content
          title="Share Resource"
          description="Share this repository or report with teammates."
        >
          <Sheet.Body>
            <div class="py-2 flex flex-wrap gap-2">
              <Button variant="outline" size="sm" leading="i-lucide:copy">
                Copy Link
              </Button>
              <Button variant="outline" size="sm" leading="i-lucide:mail">
                Email Team
              </Button>
              <Button variant="outline" size="sm" leading="i-lucide:qr-code">
                Show QR
              </Button>
            </div>
          </Sheet.Body>
        </Sheet.Content>
      </Sheet>
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-closed` | `sheet-trigger`, `sheet-content`, `sheet-overlay` | Present when disclosure or transition content is closed. |
| `data-disabled` | `sheet-trigger` | Present when the component, slot, or item is disabled. |
| `data-expanded` | `sheet-trigger`, `sheet-content`, `sheet-overlay` | Present when the panel, accordion, or menu is expanded. |
| `data-overlay-scroll` | `sheet-overlay` | Present when scrolling is owned by the overlay. |
| `data-transition` | `sheet-content` | Present while a target participates in a visibility transition. |
| `data-header` | `sheet-body` | Present when the component renders header content. |

## Props

### Sheet

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| ariaLabel | string \| undefined | — | Accessible name used when the sheet has no rendered title. |
| close | boolean \| undefined | true | Whether to show a close button. |
| closeIcon | IconT.Name \| JSX.Element \| undefined | 'icon-close' | Icon name or custom content for the close button. |
| defaultOpen | boolean \| undefined | false | Initial open state when uncontrolled. |
| dismissible | boolean \| undefined | true | Whether outside interaction and Escape should dismiss the shell. |
| id | string \| undefined | — | Unique identifier used to derive the content id. |
| inset | boolean \| null \| undefined | false | Whether the surface is inset from viewport edges. |
| modal | boolean \| undefined | true | Whether the surface contains focus, restores focus on close, hides outside content from<br>assistive technology, and prevents the native default action of outside pointer events.<br>Outside pointer and Escape dismissal remain controlled by `dismissible`. |
| onClosePrevent | (() => void) \| undefined | — | Called when a dismissal attempt is blocked. |
| onExitComplete | (() => void) \| undefined | — | Called after the modal has fully finished its exit motion. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the open state changes. |
| open | boolean \| undefined | — | Controlled open state. |
| overlay | boolean \| undefined | true | Whether to render the overlay element. |
| portalMount | Node \| undefined | — | Default destination for Modal.Portal; defaults to the trigger document body. |
| preventScroll | boolean \| undefined | true | Whether body scroll should be locked while the shell is present. |
| side | 'top' \| 'right' \| 'bottom' \| 'left' \| null \| undefined | 'right' | Edge from which the sheet opens. |
| transition | boolean \| undefined | true | Whether to enable transition animations. |
| children | JSX.Element \| undefined | — | Composed trigger and content primitives. |
| classes | Classes \| undefined | — | Family slot class defaults for this Sheet instance. |
| styles | Styles \| undefined | — | Family slot style defaults for this Sheet instance. |

### Sheet.Trigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'button' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Sheet.Content

Props for Sheet.Content.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| description | JSX.Element \| undefined | — | Secondary description displayed below the title. |
| title | JSX.Element \| undefined | — | Primary title displayed in the sheet header. |
| children | JSX.Element \| undefined | — | Composable sheet parts. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | ContentClasses \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | ContentStyles \| undefined | — | Family slot style defaults for this instance. |

### Sheet.Header

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Sheet.Title

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| id | string \| undefined | — | — |
| as | T \| undefined | 'h2' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Sheet.Description

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| id | string \| undefined | — | — |
| as | T \| undefined | 'p' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Sheet.Action

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Sheet.Body

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Sheet.Footer

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| as | T \| undefined | 'div' | — |
| children | JSX.Element \| undefined | — | — |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Sheet.Close

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'button' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
