REFERENCE
- Gallery page: https://magpui.com/en/i/bottom-sheet-spring-vs-ease-1bb88c
- First frame (image): https://dnioievkvxejjbxgwvko.supabase.co/storage/v1/object/public/media/bottom-sheet-spring-vs-ease-1bb88c/shot.jpg
- Clip (mp4): https://dnioievkvxejjbxgwvko.supabase.co/storage/v1/object/public/media/bottom-sheet-spring-vs-ease-1bb88c/clip.mp4
- Original post: https://x.com/shedsgns/status/2112046080510591293 by @shedsgns
Before building, download the first frame and look at it. If ffmpeg is available, download the clip and extract frames (ffmpeg -i clip.mp4 -vf fps=2 frame-%02d.png), then study the motion frame by frame. Match what you see; the text below describes it, the frames are the truth.
BUILD: A draggable mobile bottom sheet whose release animation is a velocity-aware spring, plus an optional comparison view that shows three copies of the same sheet (ease, smooth spring, bouncy spring) side by side, each with a small live graph of position over time. The point is the feel: when the finger lets go, the sheet keeps the finger's speed and carries it into the snap point instead of restarting from zero.
DESIGN SYSTEM: Dark canvas #0B0B0B. Each demo sits in a card #141414, radius 16px, padding 24px, about 300px wide, 3 cards in a row with 8px gap. Inside, a phone frame 220x440px, radius 32px, border 2px #2A2A2A, screen #1A1A1A with a 60x6px notch pill #2E2E2E. Content placeholders: a large block #262626 radius 12px, a list row 56px tall radius 12px with a 24px circle avatar and two text lines #3A3A3A. The sheet: surface #232323, top radius 20px, grab handle 36x4px #4A4A4A centered 8px from top, three 48px placeholder rows radius 10px #2C2C2C. Under the phone, a graph panel 220x56px, radius 10px, background #101010, a baseline and target line in #2A2A2A, the curve stroked 2px #F2D46B with a 6px dot at the current point. Labels below: title 12px medium #E5E5E5, parameters 11px #7A7A7A ("ease 0.3s", "duration 0.5 bounce 0", "duration 0.5 bounce 0.3"). Sans-serif UI typeface throughout.
MOTION: Drag: sheet translateY follows the pointer 1:1, no easing, rubber-band above the top snap point at 0.3x resistance. Release: pick the snap point (collapsed, half, expanded) from position plus projected velocity (position + velocity * 0.2s). Ease variant: translateY to target in 300ms cubic-bezier(0.25, 0.1, 0.25, 1), velocity ignored. Smooth spring: duration 500ms, bounce 0 (roughly stiffness 400, damping 40, mass 1), initial velocity = pointer velocity at release. Bouncy spring: duration 500ms, bounce 0.3 (roughly stiffness 400, damping 26), same initial velocity, one small overshoot. Graph: plot translateY progress each frame while animating, dot moves along the curve, curve fades to 40% opacity 400ms after rest. Background content under the sheet scales to 0.96 and dims to 70% as the sheet expands, interpolated from sheet progress.
SECTIONS: Parts: phone frame, background content, sheet with handle, graph, label. States: idle (sheet collapsed), dragging (follows finger, graph frozen), released (spring running, graph drawing), settled (curve faded, dot on target line), expanded (sheet near top, background scaled). Comparison mode: three cards driven by the same recorded flick so the difference is visible.
IMPLEMENTATION: Use Motion (Framer Motion) with useMotionValue for y, drag="y" with dragConstraints, and animate(y, target, { type: "spring", duration: 0.5, bounce: 0.3, velocity }) on drag end; pass the drag velocity explicitly. Draw the graph in an SVG path updated from y.on("change"). prefers-reduced-motion: snap with a 150ms opacity crossfade, no bounce. Keyboard: the handle is a button with aria-expanded; Enter/Space toggles, ArrowUp/ArrowDown move between snap points; visible 2px focus ring #F2D46B. Only animate transform and opacity; keep the graph to one path, max 120 points.
ADAPT TO THE PROJECT: Reuse the project's existing sheet or drawer component, design tokens, fonts and spacing; map the colors above to the closest existing tokens. Do not copy any logos, brand names or content from the original; use gray placeholders.
SUCCESS CHECK:
- A fast flick ends faster than a slow drag release, and the spring variants never stall at the moment of release.
- The bouncy variant overshoots the snap point once and settles within about 500ms; the ease variant never overshoots.
- With reduced motion enabled, the sheet snaps without spring motion and the keyboard can open and close it.