Overlays & menus
Sheet
Drag-to-dismiss sheet with spring physics
detent-sheet
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 3
- Wave
- phase-3-overlays
- Group
- Overlays & menus
Edit profile
Make changes, then save.
Drag the grip toward the edge to dismiss, or press Escape. Focus enters the panel and returns here.
7.3 KB gzip for the class module · inlines the shared spring
Variants
Left sheet
Slides in from the left edge with the same drag and focus contract.
Top sheet
Centred, capped at 32rem wide and 85dvh tall.
Bottom sheet
Rounded top corners, drag upward on the grip to dismiss.
The body region scrolls on its own while the sheet is open.
Recorded states
openrecorded state; no setterclosedrecorded state; no setterdraggingrecorded state; no setter
Usage
Import
import '/components/sheet/define.js';
// or register it yourself
import { DetentSheet } from '/components/sheet/sheet.js';
Attributes
default-openopenshow-closeside
Properties
onOpenChange
Slots
default
Events
detent-open-change— {}
Example
Runnable example: site/examples/sheet.ts. The snippet below is generated from the catalogued attributes.
<detent-sheet
default-open
open
show-close
side="right"
></detent-sheet>
Property-only values
const element = document.querySelector('detent-sheet');
if (element !== null) {
// onOpenChange: (open: boolean) => void
element.onOpenChange = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/sheet.tsxsha256 5b762dfb2c78…
Native files
The native module and its artifacts:
src/components/sheet/sheet.tssrc/components/sheet/styles.csssrc/components/sheet/define.tsdocs/components/sheet.mdsite/examples/sheet.tstests/browser/sheet.spec.tsevidence/components/sheet.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
default-open | defaultOpen | boolean | false | Presence semantics: present means true, absent means false. |
open | open | boolean | undefined | Presence semantics: present means true, absent means false. |
show-close | showClose | boolean | true | Presence semantics: present means true, absent means false. |
side | side | 'right' | 'left' | 'top' | 'bottom' | 'right' | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
onOpenChange | (open: boolean) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-open-change | onOpenChange | (open: boolean) => void | Composed, bubbles; fires only for a user action. |
Slots
| Slot | Notes |
|---|---|
default | Consumer content; rendered through a slot, never re-created. |
CSS customisation
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React portals replaced with native dialog/top-layer and DOM insertion.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- onOpenChange becomes the composed detent-open-change event (detail: { open }); the callback is not a property. Programmatic writes emit nothing (PC-6.2/6.5).
- Nine React exports become one tag, six slots and eleven parts. PC-2.1 is one slug ↔ one tag; SheetHeader/SheetBody become the header row and part="body", and SheetTrigger/SheetClose become slots for the consumer's own controls (PC-11.2/11.6).
- A native modal <dialog> replaces the React portal. The top layer replaces createPortal(…, document.body) and the fixed inset-0 z-50 frame, so ancestor transforms/overflow cannot clip it and Escape arrives as cancel.
- Focus management is implemented, not assumed. Upstream's catalog entry claims a focus trap, Escape, focus return and scroll lock; the source had a Tab handler but focus entry/return and the ARIA wiring are implemented here and asserted on all three engines.
- Framer Motion becomes the shared spring helper plus one distance value. The pinned spring (380/38/0.9) is preserved; interruption restarts from the current position with a fresh clock (position-continuous, velocity reset), and reduced motion jumps to the target with no frames (PC-10.2/10.5).
- The grab handle is available on all four sides. Upstream rendered it for top/bottom only and required a touch swipe anywhere for left/right; a pointer device could not drag a horizontal sheet at all. The handle keeps the same thresholds and elastic resistance and is aria-hidden (recorded as a deliberate usability correction, PC-12.6).
- Backdrop activation always closes. Upstream had no opt-out; none is added.
- lucide-react's X is a local decorative inline SVG (PC-11.5).
- Palette from tokens. Upstream used neutral-* + dark: classes; this port reads --detent-color-* so one rule set serves both schemes (PC-9.5).
What it does
A drawer that slides in from any edge, can be dragged away to dismiss with spring physics, traps focus while it is modal and returns focus to the trigger on close. It renders as a real <dialog> in the browser's top layer.
Upstream behaviour
- Four sides: right, left, top, bottom with directional drag-to-dismiss
- Touch: swipe left/right sheets anywhere; top/bottom sheets use the grab handle
- Springs back when released early; rubber-band elastic resistance
- Portal rendering, focus trap + focus return, Esc, scroll lock without layout shift
- ARIA dialog wired to SheetTitle/SheetDescription; backdrop click closes
Differences from upstream
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React portals replaced with native dialog/top-layer and DOM insertion.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- onOpenChange becomes the composed detent-open-change event (detail: { open }); the callback is not a property. Programmatic writes emit nothing (PC-6.2/6.5).
- Nine React exports become one tag, six slots and eleven parts. PC-2.1 is one slug ↔ one tag; SheetHeader/SheetBody become the header row and part="body", and SheetTrigger/SheetClose become slots for the consumer's own controls (PC-11.2/11.6).
- A native modal <dialog> replaces the React portal. The top layer replaces createPortal(…, document.body) and the fixed inset-0 z-50 frame, so ancestor transforms/overflow cannot clip it and Escape arrives as cancel.
- Focus management is implemented, not assumed. Upstream's catalog entry claims a focus trap, Escape, focus return and scroll lock; the source had a Tab handler but focus entry/return and the ARIA wiring are implemented here and asserted on all three engines.
- Framer Motion becomes the shared spring helper plus one distance value. The pinned spring (380/38/0.9) is preserved; interruption restarts from the current position with a fresh clock (position-continuous, velocity reset), and reduced motion jumps to the target with no frames (PC-10.2/10.5).
- The grab handle is available on all four sides. Upstream rendered it for top/bottom only and required a touch swipe anywhere for left/right; a pointer device could not drag a horizontal sheet at all. The handle keeps the same thresholds and elastic resistance and is aria-hidden (recorded as a deliberate usability correction, PC-12.6).
- Backdrop activation always closes. Upstream had no opt-out; none is added.
- lucide-react's X is a local decorative inline SVG (PC-11.5).
- Palette from tokens. Upstream used neutral-* + dark: classes; this port reads --detent-color-* so one rule set serves both schemes (PC-9.5).