Draggable Throw Notes — Free GSAP Inertia Snippet
Draggable Throw Notes · Cards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="dtn-board" id="dtnBoard">
<p class="dtn-hint">Drag a note and let go with speed — it keeps moving and settles inside the board.</p>
<div class="dtn-note" style="--nc:#fde68a;--nr:-3deg;left:8%;top:18%">
<b>Ship it 🚀</b>
<span>Flick me across the board.</span>
</div>
<div class="dtn-note" style="--nc:#a7f3d0;--nr:2.5deg;left:42%;top:48%">
<b>Physics ✨</b>
<span>Momentum comes from InertiaPlugin.</span>
</div>
<div class="dtn-note" style="--nc:#bfdbfe;--nr:-1.5deg;left:66%;top:22%">
<b>Bounds 🧲</b>
<span>I can't escape the corkboard.</span>
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b0d16;color:#1f2937;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.dtn-board{position:relative;width:min(640px,94vw);height:min(420px,72vh);border-radius:18px;background:radial-gradient(120% 120% at 30% 20%,#1a2036,#0e1222);border:1px solid rgba(255,255,255,.12);overflow:hidden}
.dtn-hint{position:absolute;inset:auto 0 14px 0;text-align:center;color:#5f6782;font-size:12px;letter-spacing:.05em;pointer-events:none}
.dtn-note{position:absolute;width:168px;padding:14px 14px 16px;border-radius:4px;background:var(--nc);transform:rotate(var(--nr));box-shadow:0 12px 28px rgba(0,0,0,.45);cursor:grab;user-select:none;touch-action:none;will-change:transform}
.dtn-note:active{cursor:grabbing}
.dtn-note::before{content:'';position:absolute;top:-7px;left:50%;transform:translateX(-50%);width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#f87171,#991b1b);box-shadow:0 2px 4px rgba(0,0,0,.4)}
.dtn-note b{display:block;font-size:14px;margin-bottom:5px}
.dtn-note span{font-size:12.5px;line-height:1.45;color:rgba(0,0,0,.65)}gsap.registerPlugin(Draggable, InertiaPlugin);
var z = 10;
Draggable.create('.dtn-note', {
bounds: '#dtnBoard',
// inertia hands the release velocity to InertiaPlugin, which glides
// the note to a natural stop *within bounds* — no manual physics.
inertia: true,
edgeResistance: 0.72,
onPress: function () {
// Bring the grabbed note above its siblings and lift it slightly.
this.target.style.zIndex = ++z;
gsap.to(this.target, { scale: 1.06, rotation: 0, duration: 0.18 });
},
onRelease: function () {
gsap.to(this.target, {
scale: 1,
rotation: 'random(-4, 4)',
duration: 0.4,
ease: 'power2.out'
});
}
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Draggable Throw Notes — Momentum Dragging With GSAP’s InertiaPlugin

Plain HTML5 drag feels dead: the element stops the instant your pointer does. These sticky notes feel alive — grab one, flick it, and it keeps traveling with realistic momentum before gliding to a stop, always inside the corkboard. That's GSAP's Draggable paired with InertiaPlugin (both free on the CDN since GSAP 3.13), which together replace an entire physics-and-input layer with one Draggable.create() call.
inertia: true is where the physics lives
Draggable continuously tracks pointer velocity while you drag. On release, inertia: true hands that velocity to InertiaPlugin, which computes a natural deceleration curve — how far the note *would* slide given friction — and tweens it there. Crucially, the plugin solves the end position *first*: it knows the note's destination the moment you let go, which is why it can guarantee the landing spot respects bounds instead of clamping mid-flight with an ugly wall-stop.
bounds turns the board into a container, not a cage wall
bounds: '#dtnBoard' constrains both dragging and thrown travel to the board's box. During an over-drag past the edge, edgeResistance: 0.72 makes the note move only ~28% of your pointer's distance — the rubbery tension every native app uses to say "you've hit the edge" — and on release it settles back inside. A thrown note aimed at the edge decelerates to land exactly at the boundary, because InertiaPlugin factored the bounds into its end-point solve.
Grab feedback is choreography, not physics
onPress bumps the note's z-index above its siblings (an incrementing counter, so the most recently touched note always wins) and animates a 1.06 scale with rotation zeroed — the note "lifts off the board" and squares up in your hand. onRelease settles it back down with a fresh random(-4, 4) rotation, so every throw leaves the board looking naturally messy rather than snapping notes back to fixed angles. GSAP's string-based random() makes that one line.
touch-action: none is the mobile linchpin
Each note sets touch-action: none so mobile browsers hand the touch to Draggable instead of scrolling the page — without it, vertical flicks would scroll instead of throw. Draggable normalizes mouse, touch, and pointer events internally, so the one config works everywhere, including the user-select: none needed to stop text selection during fast drags.
Transforms only, so throws stay smooth
Draggable moves elements via x/y transforms (not left/top), so dragging and inertia glides are compositor work. The CSS positions notes initially with left/top percentages purely for layout convenience; Draggable layers transforms on top from then on.
Where you'd take it further
Real boards persist positions (write this.x / this.y to storage in onThrowComplete), add snap to align notes to a grid, or set throwResistance to tune how far flicks travel. For rotation-based dragging, see the drag spin dial; for pointer-ordered lists, drag sort list; for scroll-driven momentum instead, drag scroll row; and for resizable workspaces, drag resize panels.
Step by step
How to Use
- 1Add the GSAP CDNsInclude gsap, Draggable, and InertiaPlugin from the CDN panel.
- 2Paste HTML, CSS, and JSThree sticky notes render pinned to a corkboard.
- 3Grab a noteIt lifts, squares up, and jumps above its siblings.
- 4Flick and releaseMomentum carries it to a natural stop inside bounds.
- 5Push past an edgeRubbery resistance, then it settles back in.
- 6Tune the feeledgeResistance and throwResistance set the physics.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Draggable tracks your pointer's velocity throughout the drag; with inertia: true, that velocity is handed to InertiaPlugin on release, which computes a natural deceleration tween. It solves the final resting position up front — factoring in bounds — then animates there, which is why throws glide to elegant stops instead of hitting invisible walls.
It damps over-drag: at 0.72, once your pointer crosses the board's boundary the note follows only 28% of the pointer's movement, producing the rubber-band tension native apps use to signal an edge. On release the note animates back inside bounds. Set it to 1 for a hard wall or 0 to allow free dragging outside.
Because on touch devices the browser defaults to interpreting vertical drags as page scrolling. touch-action: none tells the browser this element handles its own gestures, so flicks reach Draggable instead of scrolling. Combined with user-select: none (preventing text selection mid-drag), it's the difference between a demo that works on desktop and one that works everywhere.
An incrementing z counter: onPress assigns ++z to the grabbed note's z-index, so each grab claims a value higher than every previous one. It's simpler and cheaper than re-sorting siblings, and it matches physical intuition — whatever you touched last is the note sitting highest on the pile.
Yes — pass snap: { x: value => Math.round(value / 40) * 40, y: same } and InertiaPlugin will target the nearest snap point when solving the throw's end position, so notes glide into alignment rather than stopping arbitrarily. An array of pixel values works too for fixed slots, and liveSnap: true snaps during the drag itself.
Create the Draggable instances in a mount effect — useEffect, onMounted, or ngAfterViewInit — from a container ref, and call .kill() on each instance (or revert a gsap.context) in the cleanup so listeners detach on unmount. Register both plugins once at module scope. To persist positions, read this.x/this.y in onThrowComplete into state or storage; note styling maps directly to Tailwind utilities.