# Sheet

A panel docked to an edge of the viewport.

- Group: Overlays
- Import: `import { Sheet } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/sheet/
- Related: dialog, popover

## When to reach for it

A modal that needs room — a filter panel, a detail view. It IS a Dialog, docked; the sides are named in reading order, so `end` is the right in English and the left in Arabic.

## Anatomy

- **Scrim** (required) — The same --scrim at --z-overlay that Dialog uses — the same component, in fact, so the page behind is inert and scroll-locked exactly as it is under a dialog.
- **Panel** (required) — The docked box. start and end are a min(24rem, 92vw) column at full height; top and bottom are a full-width band capped at 85vh. A flex column that scrolls itself.
- **Title** (required) — title, typed as required rather than optional — there is no unnamed-sheet path to fall into, only a hidden-title one via hideTitle.
- **Description** — description, under the title, sharing the wrapper that hideTitle hides.
- **Close** (required) — The X in the top-end corner. Unlike Dialog there is no showClose to turn it off, so every sheet has one whichever edge it is docked to.

## Best practices

### Do

- Reach for a Sheet over a Dialog when the content is a list or a form long enough to scroll: it gets the full height of the viewport rather than Dialog’s 32rem by 85vh box, and the reader keeps the page edge as an anchor.
- Use top or bottom when the content is wide and short — a filter bar, a date range. start and end are a 24rem column, and a table pushed into one wraps into a ribbon.
- Name the sides start and end rather than reaching for left and right: each edge has its own literal class string carrying its own rtl: variant, so end arrives from the right in English and the left in Arabic with no second code path.
- Wrap the cancelling control in SheetClose so the close runs through Radix — a sheet closed by your own state setter leaves focus inside a panel that is no longer on the page.

### Don’t

- Do not re-declare the travel in className. The panel carries data-folio-animated, which removes its transform outright under reduced motion; a second transform of your own only gets the universal floor, so it still arrives — a hundredth of a millisecond later, from wherever you put it.
- An OverlayContainer whose element is not positioned docks the sheet to the wrong box: naming a container switches the panel from fixed to absolute, so an unpositioned container sends it to the nearest positioned ancestor rather than to the frame.
- It is a modal dialog, so the page behind is scroll-locked and pointer-inert: this is not the home for a filter panel the reader is meant to work alongside. That is a Popover, or a column in the layout.

## Accessibility

- Shares Dialog’s focus trap, Escape handling and scroll lock rather than reproducing them — a second focus trap is a second one to get wrong.
- The title is required, visible or not.

## Keyboard

- Escape — Closes it, and focus returns to the trigger.
- Tab — Cycles inside the sheet.

## Sheet

Re-export of `DialogPrimitive.Root`.

Radix Dialog root, trigger and close — a sheet IS a dialog, docked.

## SheetTrigger

Re-export of `DialogPrimitive.Trigger`.

## SheetClose

Re-export of `DialogPrimitive.Close`.

## SheetContent

A panel docked to an edge of the viewport. It is a modal dialog — Radix's, so the focus trap, the escape key, the scroll lock and the `aria-modal` wiring are the same ones `Dialog` gets. The only differences are where it sits and which way it arrives, which is why this shares that implementation rather than reproducing it: a second focus trap is a second focus trap to get wrong. The title is required, visible or not. A modal with no accessible name drops a screen reader into an unnamed region with no way back out. Portals into the element an enclosing `OverlayContainer` names, docking to that element's edge rather than the viewport's when there is one.

### Props

- `side` — `SheetSide` default `'end'`. Which edge it is docked to. `end` by default.
- `title` (required) — `ReactNode`.
- `description` — `ReactNode`.
- `hideTitle` — `boolean` default `false`. Hide the title visually while keeping it for assistive tech.
- `closeLabel` — `string` default `'Close'`. Accessible close action, supplied by the host locale.

Also accepts: `Omit<ComponentProps<typeof DialogPrimitive.Content>, 'title'>`.

## Example — sides

```tsx
import { Button, Field, Input, Sheet, SheetContent, SheetTrigger } from '@misoto22/folio'

<div className="flex flex-wrap gap-3">
  {(['start', 'end', 'top', 'bottom'] as const).map((side) => (
    <Sheet key={side}>
      <SheetTrigger asChild>
        <Button variant="secondary" size="sm">{side}</Button>
      </SheetTrigger>
      <SheetContent side={side} title="Filters" description="Narrow the list.">
        <Field label="Search"><Input type="search" /></Field>
      </SheetContent>
    </Sheet>
  ))}
</div>
```

## Example — room to scroll

```tsx
<Sheet>
  <SheetTrigger asChild>
    <Button variant="secondary">Filter deploys</Button>
  </SheetTrigger>
  <SheetContent side="end" title="Filter deploys" description="Six branches, one search.">
    <div className="mt-4 flex flex-col gap-4">
      <Field label="Search">
        <Input type="search" placeholder="Branch or commit" />
      </Field>
      <fieldset className="m-0 flex flex-col gap-3 border-0 p-0">
        <legend className="mb-2 p-0 eyebrow text-(--ink-3-aa)">Branch</legend>
        {BRANCHES.map((branch) => (
          <label key={branch} className="flex cursor-pointer items-center gap-2.5 text-sm text-(--ink-2)">
            <Checkbox defaultChecked={branch === 'main'} />
            {branch}
          </label>
        ))}
      </fieldset>
      <div className="flex justify-end gap-3 pt-2">
        <SheetClose asChild>
          <Button variant="secondary">Cancel</Button>
        </SheetClose>
        <SheetClose asChild>
          <Button>Apply</Button>
        </SheetClose>
      </div>
    </div>
  </SheetContent>
</Sheet>
```

## Example — a wide band

```tsx
<Sheet>
  <SheetTrigger asChild>
    <Button variant="secondary">Set the reporting range</Button>
  </SheetTrigger>
  <SheetContent side="bottom" title="Reporting range" description="Applied to every chart on the page.">
    <div className="mt-4 grid gap-4 sm:grid-cols-4">
      <Field label="From">
        <Input type="date" defaultValue="2026-07-01" />
      </Field>
      <Field label="To">
        <Input type="date" defaultValue="2026-09-30" />
      </Field>
      <Field label="Region">
        <NativeSelect defaultValue="au">
          <option value="au">Australia</option>
          <option value="jp">Japan</option>
          <option value="all">Everywhere</option>
        </NativeSelect>
      </Field>
      <Field label="Compare with">
        <NativeSelect defaultValue="previous">
          <option value="previous">Previous period</option>
          <option value="year">Same period last year</option>
          <option value="none">Nothing</option>
        </NativeSelect>
      </Field>
    </div>
    <div className="mt-6 flex justify-end gap-3">
      <SheetClose asChild>
        <Button variant="secondary">Cancel</Button>
      </SheetClose>
      <SheetClose asChild>
        <Button>Apply</Button>
      </SheetClose>
    </div>
  </SheetContent>
</Sheet>
```
