Skip to content
Monogem

Component · Containers & Overlays

Sheet

Overview

An edge-attached overlay panel — Dialog positioned flush against a screen edge instead of centered. Sheet inherits Dialog's modal behavior contract (see Focus trap and restoration) and adds edge placement and mobile-specific considerations.

Anatomy

Overlay (backdrop) → Panel → Header (Title + Description + Close control) → Body → optional Footer (actions).

  • Overlay — the scrim behind the panel.
  • Panel — the surface attached to one edge.
  • Header — Title, Description, and Close control.
  • Body — the main content.
  • Footer — optional action row.

Same part list as Dialog's Title/Description/Body/Actions/Close, arranged in a panel attached to one edge instead of a centered floating surface.

Live Example

Right (default) — a filters panel

Bottom — a mobile-style sheet

Right, unsaved changes — overlay-click disabled

Code Example

sheet-showcase.tsx
"use client";

import { X } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";

/**
* Live Sheet example — a Sheet is a Dialog attached to a screen edge, so the
* focus trap, `Escape` / overlay-click dismissal, focus restoration, and
* action ordering all behave exactly as Dialog's do.
*
* - Right (default): a desktop-style secondary panel, full height, capped
* width. Rounded on its left corners only; the right edge is flush.
* - Bottom: a mobile-style bottom sheet, full width, capped height (never the
* full viewport) — its body scrolls. No drag handle: this component doesn't
* implement swipe-to-dismiss, so it doesn't show an affordance for one.
* - Right, unsaved changes: `dismissible={false}`, so an accidental overlay
* click can't discard the edits — Cancel and `Escape` remain.
*/
export function SheetShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Right (default) — a filters panel
</h3>
<Sheet>
<SheetTrigger>
<Button variant="outline">Open filters</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
<SheetDescription>
Narrow the list without leaving the page behind it.
</SheetDescription>
<SheetClose>
<Button
variant="ghost"
size="icon"
aria-label="Close"
className="absolute right-4 top-4"
>
<X />
</Button>
</SheetClose>
</SheetHeader>
<SheetBody>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="filter-owner">Owner</Label>
<Input id="filter-owner" placeholder="Anyone" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="filter-label">Label</Label>
<Input id="filter-label" placeholder="Any label" />
</div>
</div>
</SheetBody>
<SheetFooter>
<SheetClose>
<Button variant="ghost">Reset</Button>
</SheetClose>
<SheetClose>
<Button>Apply</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Bottom — a mobile-style sheet
</h3>
<Sheet>
<SheetTrigger>
<Button variant="outline">Choose a plan</Button>
</SheetTrigger>
<SheetContent side="bottom">
<SheetHeader>
<SheetTitle>Choose a plan</SheetTitle>
<SheetDescription>
The sheet is capped below the full viewport height; this list
scrolls inside it.
</SheetDescription>
</SheetHeader>
<SheetBody>
<ul className="flex flex-col gap-3">
{Array.from({ length: 10 }, (_, i) => (
<li
key={i}
className="rounded-md border-[length:var(--scale-px)] border-solid border-border p-3"
>
Plan tier {i + 1} — a short description of what this tier
includes and who it suits.
</li>
))}
</ul>
</SheetBody>
<SheetFooter>
<SheetClose>
<Button variant="ghost">Not now</Button>
</SheetClose>
<SheetClose>
<Button>Continue</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</section>

<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Right, unsaved changes — overlay-click disabled
</h3>
<Sheet dismissible={false}>
<SheetTrigger>
<Button variant="outline">Edit profile</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Changes here aren’t saved until you choose Save.
</SheetDescription>
</SheetHeader>
<SheetBody>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="profile-name">Display name</Label>
<Input id="profile-name" defaultValue="Ada Lovelace" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="profile-title">Title</Label>
<Input id="profile-title" defaultValue="Mathematician" />
</div>
</div>
</SheetBody>
<SheetFooter>
<SheetClose>
<Button variant="ghost">Cancel</Button>
</SheetClose>
<SheetClose>
<Button>Save</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</section>
</div>
);
}

Variants

Supported placement — Version 1

PlacementAttaches toWidth/heightTypical use
Right (default)Right edge, full viewport heightFixed width, content-appropriate (e.g. capped near --breakpoint-sm)Desktop-style secondary panel — filters, details, a focused form
BottomBottom edge, full viewport widthCapped max-height (not full viewport)Mobile-style bottom sheet

Left and top placements are not supported for Version 1 — no current use case needs them, and adding placements without a real need would be speculative.

A side Sheet and a bottom Sheet are not the same component skinned two ways — they serve different contexts (desktop secondary panel vs. mobile-native pattern) and, as documented below, support different dismissal affordances. Don't conflate them.

Rounded corners by placement

Only the corners facing away from the flush edge are rounded, at --radius-xl (matching Dialog's "Modals, large surfaces" token):

PlacementRounded corners
RightTop-left and bottom-left only — the right edge is flush with the viewport
BottomTop-left and top-right only — the bottom edge is flush with the viewport

States

StateLook
ClosedNot rendered
OpenOverlay visible, panel --background fill, edge-appropriate --radius-xl corners, --shadow-lg

Usage Guidance

Relationship to Dialog

Sheet is a Dialog, placed at an edge. Everything Dialog documents for overlay behavior, focus trap, restoration, Escape/close handling, and destructive-action ordering applies to Sheet unchanged — this page documents only what differs: anatomy shape, placement, and the mobile bottom-sheet case. Use Sheet instead of a centered Dialog when retaining more of the underlying page's visual context at the edge helps, or for a mobile-style bottom-anchored flow; don't use Sheet as a universal replacement for pages — the same full-page guidance Dialog documents applies here too.

Sheet is modal by default, inheriting Dialog's full contract: overlay present, background inert, focus trapped inside the panel while open, focus restored to the trigger on close. A non-modal edge panel is a different pattern (closer to a wide Popover) and isn't what "Sheet" means in this system.

Sheet also inherits Dialog's conditional overlay-click dismissal unchanged: standard, low-risk Sheets may allow overlay-click dismissal; a Sheet used for a destructive confirmation, an unsaved-change flow, or a critical acknowledgement disables it, the same as Dialog.

Drag handle and swipe gesture

Not assumed by default. A drag handle (a small horizontal pill at the top of the panel) is a common mobile convention for the Bottom placement specifically, and may be added only when the implementation genuinely supports swipe-to-dismiss — the handle is a visual affordance for a real gesture, not decoration. Without swipe support, don't render a handle; it would promise an interaction that doesn't exist.

The Right placement never gets a drag handle — dragging a desktop side panel isn't an established interaction, and dismissal there is the same as Dialog: close control, Escape, or overlay click.

Long content and internal scrolling

Same as Dialog: Header and Footer stay fixed; only the Body scrolls internally once content exceeds the panel's available height. The Bottom placement's capped max-height (rather than full viewport height) makes this more likely to apply than on a full-height Right panel.

Tokens

Sheet reuses Dialog's token set with edge-specific radius application; it introduces no component-specific styling values.

TokenWhere used
--background / --foregroundPanel fill + text
--muted-foregroundDescription text
--borderPanel edge, useful on dark mode
--radius-xlCorner radius, applied only to the two non-flush corners
--shadow-lgElevation — Shadows names "Modals, dialogs, sheets"
--ringFocus ring on interactive content and the close control
--scale-6Panel padding
--scale-4Gap between Header/Body/Footer

The overlay scrim reuses Dialog's explicit rgb(0 0 0 / 48%) value unchanged — see Dialog: Overlay scrim; it isn't a token.

Do / Don’t

Do

  • Treat Sheet as Dialog's edge-attached form — reuse its focus, dismissal, and action-ordering rules rather than reinventing them.
  • Cap the Bottom placement's height — never let it consume the full viewport height by default.
  • Only add a drag handle on the Bottom placement, and only when swipe-to-dismiss is actually wired up.

Don’t

  • Add Left or Top placements without a real, repeated Version 1 need.
  • Give a Right-placed Sheet a drag handle or swipe affordance.
  • Use Sheet as a general page-replacement pattern — see Dialog's full-page guidance.

Accessibility

Inherits Dialog's accessibility contract in full: role="dialog" with aria-modal="true", aria-labelledby/aria-describedby referencing Title/Description, focus trap and restoration, and background inertness while open. No additional roles are introduced by edge placement — placement is a visual/layout decision, not a semantic one.

Responsive Behavior

The Bottom placement is the mobile-appropriate pattern for Sheet. In Version 1 a Right-placed Sheet does not convert itself to a Bottom presentation on a narrow viewport: it is capped at --breakpoint-sm wide, so below that it simply becomes full-width. Choosing Bottom for a mobile-first surface is the caller's decision, in line with the caution Dialog records about not silently merging distinct responsive patterns.

Related Components / Patterns

  • Dialog — the centered modal counterpart.
  • Popover — non-modal supplemental content.
  • Navbar — uses a Sheet for its mobile menu.