Source: https://monarchui.dev/components/sheet/

Components · Overlays

# Sheet

Side or bottom panel with focus trap, scroll lock, sizes and safe areas.

@rookcom/monarch-react

Component

## Record details

## Usage examples

### Filters from the left, outside clicks locked

### Bottom sheet

## Purpose

A modal panel attached to an edge of the screen: a record’s details next to the list it came from, filters, a longer form, or navigation on a phone. It keeps the page in view while focus stays in the panel.

### Use when

- Showing a record’s details without leaving the list.
- A form or settings panel too long for a dialog.
- Filters or navigation on narrow screens (left or bottom).

### Avoid when

- A short decision: use Dialog or ConfirmDialog.
- Content the person needs alongside the page for a long time: use a SplitLayout pane.
- A centred dialog that should become a bottom sheet on phones: use ResponsiveDialog.

## Design principles

- A sheet is a dialog: one title, focus trapped inside, Escape closes it, focus returns to the trigger.
- Header and footer stay put; only SheetBody scrolls, so actions are always reachable.
- Side sheets never exceed the screen width, and every edge that touches the screen pads for notches and home bars.
- Calm surface: the raised background, one border on the inner edge and the standard overlay.

## API reference

```tsx
import { Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `Sheet` | component | client | `.mn-overlay` |
| `SheetBody` | component | client | `.mn-overlay` |
| `SheetClose` | component | client | `.mn-overlay` |
| `SheetContent` | component | client | `.mn-overlay` |
| `SheetDescription` | component | client | `.mn-overlay` |
| `SheetFooter` | component | client | `.mn-overlay` |
| `SheetHeader` | component | client | `.mn-overlay` |
| `SheetTitle` | component | client | `.mn-overlay` |
| `SheetTrigger` | component | client | `.mn-overlay` |

`Sheet`, `SheetTrigger` and `SheetClose` are the Radix Dialog Root, Trigger and Close.

`SheetContent` renders in a portal with the overlay and an optional built-in close button.

SheetHeader, SheetTitle, SheetDescription, SheetBody and SheetFooter build on the Dialog parts, so typography and spacing match Dialog.

Generated from the TypeScript declarations of `packages/react/src/components/overlays/sheet`.

## Properties

`busy`, `closeOnOutsideClick`, `escapeGuard` and `footer` behave exactly as on DialogContent.

Pass `modal={false}` to `Sheet` for a panel that leaves the page interactive (no overlay, no scroll lock, no focus trap).

### `Sheet`

A panel that slides in from an edge of the screen: detail views, filters, longer forms and mobile navigation. It is a modal dialog (Radix Dialog), so focus is trapped, the page behind does not scroll, Escape closes it and focus returns to the trigger. Every sheet needs a SheetTitle.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `React.ReactNode` |  |

### `SheetBody`

The scrolling region between header and footer.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `SheetClose`

| Prop | Type | Description |
| --- | --- | --- |
| `asChild` | `boolean` |  |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `disabled` | `boolean` |  |
| `id` | `string` |  |

### `SheetContent`

| Prop | Type | Description |
| --- | --- | --- |
| `asChild` | `boolean` |  |
| `busy` | `boolean` | An action is in progress: Escape, outside clicks and the built-in close button no longer close the sheet, and the content is `aria-busy`. |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `closeLabel` | `string \| null` | Accessible label of the built-in close button. Pass null to hide the button. |
| `closeOnOutsideClick` | `boolean` | Whether a click outside closes the sheet. Default `true`. |
| `escapeGuard` | `boolean` | Ignore Escape while focus is on a control whose own popup is open (a combobox or menu button with `aria-expanded="true"`). Default `true`. |
| `footer` | `ReactNode` | Actions in a SheetFooter after the children; it stays visible while SheetBody scrolls. |
| `handle` | `boolean` | Show the grab handle on a bottom sheet. Decorative; default `true` for `bottom`. |
| `id` | `string` |  |
| `side` | `SheetSide` | The edge the sheet is attached to. Default `right`. |
| `size` | `SheetSize` | Width for `left`/`right` (20, 28, 36 or 48rem, or the full screen) and the maximum height for `top`/`bottom` (40, 60, 80 or 92% of the screen, or all of it). Default `md`. Side sheets never exceed the screen width. |

### `SheetDescription`

Becomes the sheet's accessible description.

| Prop | Type | Description |
| --- | --- | --- |
| `asChild` | `boolean` |  |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `SheetFooter`

Sticky actions at the bottom of the sheet.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `SheetHeader`

Title and description block at the top of the sheet.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `SheetTitle`

The sheet's accessible name. Required; wrap in VisuallyHidden to hide it.

| Prop | Type | Description |
| --- | --- | --- |
| `asChild` | `boolean` |  |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `SheetTrigger`

| Prop | Type | Description |
| --- | --- | --- |
| `asChild` | `boolean` |  |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `disabled` | `boolean` |  |
| `id` | `string` |  |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `side` | `'right'``'left'``'top'``'bottom'` | The edge it is attached to; default `right`. Exposed as `data-side`. |
| `size` | `'sm'``'md'``'lg'``'xl'``'full'` | Left/right: width 20, 28 (default), 36 or 48rem, or the full screen. Top/bottom: maximum height 40, 60, 80 or 92% of the screen, or all of it. Exposed as `data-size`. |
| `handle` | `'true'``'false'` | Decorative grab handle; on by default for bottom sheets only. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Open / closed | `data-state` | From Radix; the panel slides in from its edge. |
| Busy | `data-busy, aria-busy` | Escape, outside clicks and the close button do nothing while an action runs. |
| Without close button | – | `closeLabel={null}`; give the person another way to close it. |

## Interaction behaviour

- Opening moves focus into the sheet; Tab and Shift+Tab stay inside it.
- The page behind does not scroll while a modal sheet is open.
- Escape, the close button, a SheetClose or a click on the overlay closes it; focus returns to the trigger.
- `closeOnOutsideClick={false}` keeps it open on an overlay click (forms with typed values).
- Controlled with `open` + `onOpenChange` (for example from a URL parameter), or uncontrolled.

## Keyboard support

| Keys | Action |
| --- | --- |
| Enter / Space | On the trigger, opens the sheet. |
| Tab / Shift+Tab | Cycles focus within the sheet. |
| Escape | Closes the sheet and returns focus to the trigger. |

## Accessibility

Role

dialog (aria-modal)

Accessible name

SheetTitle; SheetDescription becomes the accessible description.

- Content outside the sheet is hidden from assistive technology while open (Radix).
- The built-in close button is named by `closeLabel` (default "Close") and is disabled while busy.
- The grab handle is `aria-hidden`; it is not a control.

## Responsive behaviour

- Side sheets are `min(size, 100vw)` wide: on a phone a `md` sheet fills the width.
- Top and bottom sheets span the width and grow with their content up to the size cap; the body scrolls beyond that.
- Padding follows `env(safe-area-inset-*)` on every edge that touches the screen.
- Height uses `dvh`, so mobile browser bars do not hide the footer.

## Theme support

- Raised surface, subtle border, overlay scrim and shadow tokens; light, dark and more-contrast themes apply automatically.
- The slide-in uses duration tokens, which are 0ms under `prefers-reduced-motion`.

## Do and don't

### Do

- Always include a SheetTitle (wrap it in VisuallyHidden if the design has none).
- Put actions in SheetFooter or `footer` so they stay visible.
- Set `busy` while saving, and lock outside clicks on forms.

### Don't

- Do not stack a sheet on top of another sheet.
- Do not use a full-screen sheet on desktop where a page would do.
- Do not rely on the grab handle: swipe-to-dismiss is not implemented.

## Related components

- [Dialog](https://monarchui.dev/components/dialog/): Centred modal for short tasks; Sheet builds on its parts.
- [ResponsiveDialog](https://monarchui.dev/components/responsive-dialog/): A Dialog on wide screens that becomes a bottom sheet on phones.
- [SplitLayout](https://monarchui.dev/components/split-layout/): A persistent side pane that is part of the page, not modal.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `@radix-ui/react-dialog``react` |
| Monarch components | [Dialog](https://monarchui.dev/components/dialog/) |

Reuses the `.mn-overlay` scrim and `.mn-dialog__close` button styles from dialog.css.

Dismissal rules are shared with Dialog through `src/internal/dismiss.ts`.

## Testing coverage

- Opens from its trigger, traps focus, closes on Escape and restores focus; axe passes on the open sheet.
- Opens from the keyboard; a SheetClose closes it.
- Locks page scroll while open.
- Closes on an overlay click unless `closeOnOutsideClick={false}`.
- `busy` blocks every dismissal and disables the close button.
- Controlled through `open` + `onOpenChange`; `side` and `size` are exposed as data attributes.
- Grab handle on bottom sheets only, removable with `handle={false}`.
- `footer` prop and `closeLabel={null}`.
- Non-modal sheets render no overlay and no scroll lock.
- Renders on the server (closed and open).

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: gallery/sheet, overlays/sheet-light (open, 1280) and overlays/sheet-light-390 (open, phone); Cross-browser axe: /components/sheet |

Tests: `packages/react/src/components/overlays/sheet`

## All example code

### Record details

```tsx
import { Button, DescriptionItem, DescriptionList, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@rookcom/monarch-react';

<Sheet>
  <SheetTrigger asChild>
    <Button>Open details</Button>
  </SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Invoice INV-1042</SheetTitle>
      <SheetDescription>Sent on 12 March to the billing contact.</SheetDescription>
    </SheetHeader>
    <SheetBody>
      <DescriptionList>
        <DescriptionItem term="Status">Awaiting payment</DescriptionItem>
        <DescriptionItem term="Due">26 March</DescriptionItem>
        <DescriptionItem term="Owner">Priya Raman</DescriptionItem>
      </DescriptionList>
    </SheetBody>
    <SheetFooter>
      <SheetClose asChild>
        <Button>Close</Button>
      </SheetClose>
      <Button variant="primary">Send reminder</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
```

### Filters from the left, outside clicks locked

```tsx
import { Button, Field, FieldLabel, Input, Sheet, SheetBody, SheetClose, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@rookcom/monarch-react';

// FilterIcon: demo icon from the docs site; use your own.

<Sheet>
  <SheetTrigger asChild>
    <Button leadingIcon={<FilterIcon />}>Filters</Button>
  </SheetTrigger>
  <SheetContent
    side="left"
    size="sm"
    closeOnOutsideClick={false}
    footer={
      <>
        <SheetClose asChild>
          <Button>Cancel</Button>
        </SheetClose>
        <Button variant="primary">Apply</Button>
      </>
    }
  >
    <SheetHeader>
      <SheetTitle>Filters</SheetTitle>
    </SheetHeader>
    <SheetBody>
      <Field>
        <FieldLabel>Assignee</FieldLabel>
        <Input placeholder="Anyone" />
      </Field>
    </SheetBody>
  </SheetContent>
</Sheet>
```

### Bottom sheet

```tsx
import { Button, Field, FieldLabel, Input, Sheet, SheetBody, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@rookcom/monarch-react';

<Sheet>
  <SheetTrigger asChild>
    <Button>Share</Button>
  </SheetTrigger>
  <SheetContent side="bottom" size="sm">
    <SheetHeader>
      <SheetTitle>Share board</SheetTitle>
      <SheetDescription>Anyone with the link can view.</SheetDescription>
    </SheetHeader>
    <SheetBody>
      <Field>
        <FieldLabel>Link</FieldLabel>
        <Input readOnly value="https://example.com/b/q3-launch" />
      </Field>
    </SheetBody>
  </SheetContent>
</Sheet>
```
