crucıble

Hatch

Bottom sheet that closes by physics: pull the machined handle down and release past a distance or velocity threshold to dismiss along your throw, else it springs back. Scrim coupled to sheet position, logarithmic over-pull, critically-damped snap-back, focus-trapped and scroll-locked.

motionfree

Installation

CLI

npx shadcn@latest add @crucible/hatch

Props

PropTypeDefaultDescription
childrenReact.ReactNodeSheet body — scrolls independently of the drag handle.
triggerReact.ReactNodeOptional element that opens the sheet on click (uncontrolled convenience). Controlled callers can drive open directly and omit this.
openbooleanControlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => voidFires whenever the sheet opens or closes, controlled or not.
onClose() => voidFires once the sheet has fully dismissed (after the exit animation).
onSnap(index: number) => voidFires when the sheet settles onto a snap point, with that point's index.
snapPointsnumber[][1]Rest positions as fractions of the sheet's height, 1 = fully open, 0.5 = half-revealed. Sorted internally; the largest is the open seat. Arrow keys on the handle nudge between them.
dismissDistancenumber120Downward pull past the lowest snap, in px, that dismisses on release.
dismissVelocitynumber600Downward release velocity, in px/s, that dismisses regardless of distance.
dragSurface"handle" | "sheet""handle"Which surface starts a drag. "handle" keeps gesture and body-scroll apart.
scrimOpacitynumber0.6Peak scrim opacity when fully open (coupled to sheet position).
scrimBlurnumber2Peak scrim backdrop blur in px when fully open.
dismissOnScrimClickbooleantrueClick the scrim to dismiss.
titlestring"Sheet"Accessible label for the dialog.
containerHTMLElement | nulldocument.body (fixed, full-viewport, scroll-locked)Portal target. When provided (must be a positioned element) the sheet is absolutely contained within it and page scroll is left untouched — handy for previews.
Also accepts all props of Omit<React.ComponentPropsWithoutRef<"div">, "title">they pass through to the underlying element.

Honors prefers-reduced-motion with a designed static fallback, and passes className through.