Components · Overlays
Sheet
Side or bottom panel with focus trap, scroll lock, sizes and safe areas.
Record details
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>Usage examples
Filters from the left, outside clicks locked
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
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>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';| 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 amdsheet 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
footerso they stay visible. - Set
busywhile 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: 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
| Kind | Depends on |
|---|---|
| npm packages | @radix-ui/react-dialogreact |
| Monarch 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}. busyblocks every dismissal and disables the close button.- Controlled through
open+onOpenChange;sideandsizeare exposed as data attributes. - Grab handle on bottom sheets only, removable with
handle={false}. footerprop andcloseLabel={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