Skip to content

Components · Overlays

Sheet

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

@rookcom/monarch-reactComponent

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

import { Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@rookcom/monarch-react';
ExportKindRenderingRoot class
Sheetcomponentclient.mn-overlay
SheetBodycomponentclient.mn-overlay
SheetClosecomponentclient.mn-overlay
SheetContentcomponentclient.mn-overlay
SheetDescriptioncomponentclient.mn-overlay
SheetFootercomponentclient.mn-overlay
SheetHeadercomponentclient.mn-overlay
SheetTitlecomponentclient.mn-overlay
SheetTriggercomponentclient.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.

PropTypeDescription
childrenReact.ReactNode

SheetBody

The scrolling region between header and footer.

PropTypeDescription
childrenReactNode
classNamestring
idstring

SheetClose

PropTypeDescription
asChildboolean
childrenReactNode
classNamestring
disabledboolean
idstring

SheetContent

PropTypeDescription
asChildboolean
busybooleanAn action is in progress: Escape, outside clicks and the built-in close button no longer close the sheet, and the content is `aria-busy`.
childrenReactNode
classNamestring
closeLabelstring | nullAccessible label of the built-in close button. Pass null to hide the button.
closeOnOutsideClickbooleanWhether a click outside closes the sheet. Default `true`.
escapeGuardbooleanIgnore Escape while focus is on a control whose own popup is open (a combobox or menu button with `aria-expanded="true"`). Default `true`.
footerReactNodeActions in a SheetFooter after the children; it stays visible while SheetBody scrolls.
handlebooleanShow the grab handle on a bottom sheet. Decorative; default `true` for `bottom`.
idstring
sideSheetSideThe edge the sheet is attached to. Default `right`.
sizeSheetSizeWidth 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.

PropTypeDescription
asChildboolean
childrenReactNode
classNamestring
idstring

SheetFooter

Sticky actions at the bottom of the sheet.

PropTypeDescription
childrenReactNode
classNamestring
idstring

SheetHeader

Title and description block at the top of the sheet.

PropTypeDescription
childrenReactNode
classNamestring
idstring

SheetTitle

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

PropTypeDescription
asChildboolean
childrenReactNode
classNamestring
idstring

SheetTrigger

PropTypeDescription
asChildboolean
childrenReactNode
classNamestring
disabledboolean
idstring

Variants

PropValuesDescription
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

StateExposed asBehaviour
Open / closeddata-stateFrom Radix; the panel slides in from its edge.
Busydata-busy, aria-busyEscape, 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

KeysAction
Enter / SpaceOn the trigger, opens the sheet.
Tab / Shift+TabCycles focus within the sheet.
EscapeCloses 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.
  • Dialog: Centred modal for short tasks; Sheet builds on its parts.
  • ResponsiveDialog: A Dialog on wide screens that becomes a bottom sheet on phones.
  • SplitLayout: A persistent side pane that is part of the page, not modal.

Dependencies

KindDepends on
npm packages@radix-ui/react-dialogreact
Monarch componentsDialog

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).
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual 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