Draggable Bottom Sheet Modal
A modal sheet you can drag down to dismiss, with a distance + velocity threshold.
- Category
- Patterns
- Added
- 2026-08-22
- Deps
- 1
- Updated
- 2026-09-01
Props
"use client";
import { AnimatePresence, motion } from "framer-motion";
export function BottomSheet({
open,
onClose,
title = "Filters",
children,
}: {
open: boolean;
onClose: () => void;
title?: string;
children?: React.ReactNode;
}) {
return (
<AnimatePresence>
{open && (
<>
<motion.div
key="scrim"
className="fixed inset-0 z-40 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div
key="sheet"
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={{ top: 0, bottom: 0.5 }}
onDragEnd={(_, info) => {
if (info.offset.y > 80 || info.velocity.y > 500) onClose();
}}
initial={{ y: "100%" }}
animate={{ y: 0 }}
exit={{ y: "100%" }}
transition={{ type: "spring", stiffness: 340, damping: 34 }}
className="fixed inset-x-0 bottom-0 z-50 cursor-grab touch-none rounded-t-2xl bg-white p-5 pb-8 active:cursor-grabbing"
>
<div className="mx-auto mb-4 h-1 w-10 rounded-full bg-neutral-300" />
<p className="text-lg font-bold">{title}</p>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
}
About this pattern
Getting a bottom sheet's drag-to-dismiss gesture right is mostly about not fighting the physics: dragConstraints locks vertical movement to the resting position so it cannot be dragged permanently away, dragElastic gives rubber-band resistance only when dragging down, and onDragEnd checks drag distance or release velocity, either one alone enough to trigger dismissal. A fast short flick and a slow long drag both read as the user meaning to close it, and checking distance alone would make quick flicks feel unresponsive. If neither threshold clears, there is no manual snap-back animation to write; Framer's own spring return from the drag constraints handles it automatically. Reach for this over a plain centered modal when the content is naturally mobile and bottom-anchored, filters or an action sheet, and a drag gesture is an expected interaction; for a form or anything requiring careful reading, a centered dialog that cannot be flicked away accidentally is safer. The scrim's onClick and the drag dismissal both wire to the same onClose, so a consuming app only needs to own the open boolean, not track which dismissal path fired.
Curator’s note
Dismissal checks both drag distance AND release velocity, either one sufficient on its own — a fast short flick and a slow long drag both feel like "the user meant to dismiss this" even though only one of the two thresholds fires for each. Checking only distance would make quick flicks feel unresponsive.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Native Sheet Dialog
patternsA sheet on the native dialog element — no hand-rolled focus trap, and the three things the platform leaves you fixed.
Drag Inertia Carousel
patternsA rail you can throw — it projects the momentum and settles on the nearest slide.
Swipe Card Stack
patternsA draggable deck of cards you flick away to cycle through, Tinder-style.