Detent web components
By Shane Logsdon

Overlays & menus

Sheet

Implemented

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
Open right

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

Open left Open top Open bottom

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

  • open recorded state; no setter
  • closed recorded state; no setter
  • dragging recorded state; no setter

Usage

Import

import '/components/sheet/define.js';

// or register it yourself
import { DetentSheet } from '/components/sheet/sheet.js';

Attributes

  • default-open
  • open
  • show-close
  • side

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.tsx sha256 5b762dfb2c78…

Native files

The native module and its artifacts:

  • src/components/sheet/sheet.ts
  • src/components/sheet/styles.css
  • src/components/sheet/define.ts
  • docs/components/sheet.md
  • site/examples/sheet.ts
  • tests/browser/sheet.spec.ts
  • evidence/components/sheet.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
default-opendefaultOpenbooleanfalsePresence semantics: present means true, absent means false.
openopenbooleanundefinedPresence semantics: present means true, absent means false.
show-closeshowClosebooleantruePresence semantics: present means true, absent means false.
sideside'right' | 'left' | 'top' | 'bottom''right'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
onOpenChange(open: boolean) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-open-changeonOpenChange(open: boolean) => voidComposed, bubbles; fires only for a user action.

Slots

Slots
SlotNotes
defaultConsumer 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).
Previous: SettleModal Next: Shooting Stars

Docs

Components

Project