Skip to main content

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#

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>
  )
}

Playground#

Props
Slots

Anatomy#

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:

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

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

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.

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
Esc Requests dismissal when dismissible={true}.
⇥ Moves focus forward; modal sheets contain focus.
Shift + Tab Moves focus backward; modal sheets contain focus.

Examples#

Dismiss control#

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#

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
data-closedSlot: sheet-trigger, sheet-content, sheet-overlayDescription: Present when disclosure or transition content is closed.
data-disabledSlot: sheet-triggerDescription: Present when the component, slot, or item is disabled.
data-expandedSlot: sheet-trigger, sheet-content, sheet-overlayDescription: Present when the panel, accordion, or menu is expanded.
data-overlay-scrollSlot: sheet-overlayDescription: Present when scrolling is owned by the overlay.
data-transitionSlot: sheet-contentDescription: Present while a target participates in a visibility transition.
data-headerSlot: sheet-bodyDescription: Present when the component renders header content.

Props#

Sheet#

Prop

Trigger#

Renders a <button> element by default.

Prop

Content#

Props for Sheet.Content. Renders a <div> element by default.

Prop

Header#

Renders a <div> element by default.

Prop

Title#

Renders a <h2> element by default.

Prop

Description#

Renders a <p> element by default.

Prop

Action#

Renders a <div> element by default.

Prop

Body#

Renders a <div> element by default.

Prop

Renders a <div> element by default.

Prop

Close#

Renders a <button> element by default.

Prop