Motion UIComponentsComponent

Sheet

Motion's bottom-sheet choreography on the native HTML dialog element, with a 50px fade-up entrance plus drag and flick dismissal. Keep SheetBackdrop and SheetPanel as direct Sheet children.

npx shadcn@latest add @motion/sheet

API reference

Components

<Sheet />

The coordinating root: adapts controlled or uncontrolled state to a native <dialog>, shares the drag position and element refs through context, and keeps the overlay mounted until its Motion exit animation completes.

children?

ReactNode

The trigger, backdrop, panel and your resting content. Keep the backdrop and panel as direct children so the root can place them in the dialog.

open?

boolean

Controlled open state. Omit for uncontrolled (drive it via SheetTrigger / SheetClose / the drag).

defaultOpen?

boolean = false

Initial open state when uncontrolled. Defaults to false.

onOpenChange?

(open: boolean) => void

Called whenever the sheet requests an open-state change (trigger, close button, scrim, Escape, drag-dismiss). Required to observe controlled changes.

<SheetTrigger />

The styled trigger that opens the sheet: a primary fill with the theme's snap hover/tap feel and the shared focus ring.

children?

ReactNode

The trigger's contents (label, and optionally an icon).

className?

string

Merged onto the trigger button, last, so it wins.

<SheetBackdrop />

The dimming scrim behind the open sheet. Its opacity follows the sheet's drag position so it un-dims as the sheet is pulled down.

className?

string

The scrim surface class, merged onto the shared Backdrop. Defaults to the universal bg-black modal scrim; pass a section-local veil (e.g. a color-mix(in srgb, var(--background) 72%, transparent) class) to tint it to your ground colour in both modes.

label?

string

The scrim's accessible label. When set, the scrim is a keyboard-reachable dismiss button; omit it for a decorative scrim.

<SheetPanel />

The draggable spring panel. Fades up 50px on open (theme gentle), is draggable on the y-axis with a bottom-weighted elastic, and dismisses on a downward drag past dismissOffset OR a flick past dismissVelocity. Modal semantics come from the native dialog owned by Sheet.

children?

ReactNode

The sheet's contents - your handle, header, close button and rows.

dismissOffset?

number = 92

Downward drag distance (px) past which releasing dismisses the sheet. Defaults to 92.

dismissVelocity?

number = 840

Downward flick velocity (px/s) past which releasing dismisses the sheet. Defaults to 840.

labelledBy?

string

The id of the element that labels the dialog, wired to aria-labelledby. Put this same id on your sheet's heading. Defaults to "velocity-sheet-title".

className?

string

Merged onto the panel, last, so it wins.

<SheetHandle />

The decorative grab affordance at the top of the sheet: a short pill, aria-hidden (a mouse/touch handle, not a control). Its tone is a faint, below-AA derived ink - fine here because the mark is decorative and hidden from assistive tech.

className?

string

Merged onto the centring row, last, so it wins.

<SheetClose />

The close button, top-right of the sheet. Carries the close ref, so focus lands here when the sheet opens; the theme's snap colour fade on hover and the shared focus ring. Renders a default × glyph, overridable via children.

children?

ReactNode

The button's contents. Defaults to a close (×) glyph.

label?

string = "Close"

The accessible label. Defaults to "Close".

className?

string

Merged onto the button, last, so it wins.

Hooks

useSheet

Reads the enclosing sheet's open state (and setter), or drives the sheet from your own control. The native dialog handles the modal background state. Must be used inside Sheet.

useSheet(): UseSheet

Returns

open

boolean

setOpen

(open: boolean) => void

Functions

shouldDismissSheet

Whether a drag release should dismiss the sheet: a downward drag past dismissOffset (px) OR a downward flick past dismissVelocity (px/s). Both thresholds are downward-only (positive y is down), so an upward over-drag never dismisses. Exported so the dismiss contract is unit-testable without a browser (the drag gesture itself is browser-only).

shouldDismissSheet(offsetY: number, velocityY: number, dismissOffset: number, dismissVelocity: number): boolean