Skip to content

Draggable Bottom Sheet Modal

A modal sheet you can drag down to dismiss, with a distance + velocity threshold.

Freeglassminimal
Category
Patterns
Added
2026-08-22
Deps
1
Updated
2026-09-01
Preview

Props

Requires framer-motion
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
NewPro

A sheet on the native dialog element — no hand-rolled focus trap, and the three things the platform leaves you fixed.

dialogmodalsheet
Featured
New

A rail you can throw — it projects the momentum and settles on the nearest slide.

dragcarouselinertia
Featured

A draggable deck of cards you flick away to cycle through, Tinder-style.

dragswipestack