webdevpuneet.com

Popmotion Swipe-Dismiss Stack — Tinder-Style Card Deck Snippet

Popmotion Swipe-Dismiss Stack · Cards · Plain HTML, CSS & JS · Live preview

CategoryCards

Copy & paste

Source Code

Requires
<div class="psd-stage">
  <div class="psd-head">
    <span class="psd-tag">Popmotion · swipe deck</span>
    <h2>Swipe Deck</h2>
    <p>Drag a card past the threshold and let go — it flies off carrying your release velocity, revealing the next one.</p>
  </div>
  <div class="psd-deck" id="psdDeck"></div>
  <div class="psd-actions">
    <button class="psd-btn is-no" id="psdNo">✕ Pass</button>
    <button class="psd-btn is-reset" id="psdReset">Reset deck</button>
    <button class="psd-btn is-yes" id="psdYes">❤ Like</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Velocity-carried exit
The dismissal animation is seeded with the actual release velocity via Popmotion decay, not a fixed tween.
Threshold-based decision
A single distance comparison at pointerup decides dismiss vs spring-back.
Live stamp feedback
Like/Nope stamps fade in proportionally to drag distance toward the threshold.
Rotation tied to drag
The card tilts proportionally to horizontal offset for a pivoting, tactile feel.
Spring-back under threshold
Aborted drags return to center via spring physics, not an instant snap.
Deck re-stacking animation
Promoting the next card animates its scale/offset into place with a spring.
Programmatic swipe buttons
Like/Pass buttons trigger the identical decay-based exit as a real drag.
Empty state
A dashed placeholder appears once the deck is exhausted, with a reset action.

About this UI Snippet

Popmotion Swipe-Dismiss Stack — Velocity-Carried Dismissal, Explained

Screenshot of the Popmotion Swipe-Dismiss Stack snippet rendered live

A convincing swipe-to-dismiss deck needs three things working together: a drag that tilts and offsets the card, a threshold decision about whether the drag counts as a dismissal, and — the part that's easy to get wrong — an exit animation that actually continues the motion the user started, instead of resetting to a fixed "fly away" tween. This snippet handles all three with Popmotion.

Threshold decision at release

Every pointermove updates pos.x/pos.y directly and applies a rotation proportional to horizontal offset (pos.x / 18 degrees) so the card visibly tilts as it's dragged, like a card pivoting from your thumb. On pointerup, the only decision that matters is one comparison:

var pastThreshold = Math.abs(pos.x) > THRESHOLD;

Past the threshold, the card is dismissed. Under it, it springs back to center. This binary branch is the entire "swipe logic" — everything else is animation.

Why the exit uses decay, seeded with real velocity

The naive dismissal animation is a fixed tween: animate translateX from wherever the card is to some far-off value over, say, 300ms. That looks fine for a slow deliberate swipe but wrong for a fast flick — the card visibly *decelerates* into the tween's easing curve even though the user just flung it at speed. This snippet instead reuses the release velocity (vx, tracked the same way as in the Popmotion Drag Inertia Card) as the starting velocity of a decay animation:

popmotion.animate({ keyframes: [pos.x], velocity: Math.abs(vx) > 40 ? vx : dir * 900, type: 'decay', power: 0.6, timeConstant: 300, onUpdate, onComplete })

A hard flick keeps its speed into the exit; a slow drag that merely crossed the threshold gets a velocity floor (dir * 900) so it doesn't limp off-screen too slowly — the ternary guarantees a *minimum* exit speed regardless of how the threshold was crossed, while still respecting a genuinely fast flick's higher velocity.

Stamps as a threshold visualization

likeStamp.style.opacity = pos.x > 0 ? t : 0 where t = Math.min(Math.abs(pos.x) / THRESHOLD, 1) fades in a "Like"/"Nope" stamp proportionally to how close the drag is to the dismissal threshold — it reaches full opacity exactly when pos.x reaches THRESHOLD, giving a visual preview of what will happen if the user lets go right now.

Promoting the next card

onComplete: advanceDeck fires only once the decay animation actually finishes (falls below its rest speed), not the instant the drag ends — so the flung card is genuinely off-screen before it's removed from the DOM. advanceDeck() then removes the top card's element, shifts the data array, and re-numbers the remaining cards' z-index/scale/offset so the second card becomes the new top — animated into place with its own small spring rather than an instant CSS snap, so the deck settles rather than jump-cutting.

Reusing it

This threshold-plus-velocity-carried-exit pattern generalizes to any swipe-to-act UI: email archive/delete swipes, image approve/reject queues, onboarding card stacks. Pair it with the Popmotion Drag Inertia Card for the single-card version of the same velocity tracking without the deck logic.

Step by step

How to Use

  1. 1
    Add the Popmotion CDNInclude the popmotion UMD build for the global popmotion.animate function.
  2. 2
    Paste HTML, CSS, and JSA five-card deck renders with the frontmost card active and draggable.
  3. 3
    Drag a card sidewaysIt tilts and offsets with the pointer, and a Like/Nope stamp fades in as you approach the threshold.
  4. 4
    Release past the thresholdThe card flies off using a decay animation seeded with your release velocity.
  5. 5
    Release under the thresholdA spring animation pulls the card back to center instead of dismissing it.
  6. 6
    Use the buttons or resetPass/Like buttons trigger the same programmatic dismissal; Reset deck rebuilds the stack.

Real-world uses

Common Use Cases

Dating and matching decks
Build the canonical swipe-to-decide stack, where the card tilts with the drag and Like and Nope stamps fade in proportionally.
Review and triage queues
Approve or reject photos, applications or posts, with a single distance comparison at pointer release deciding whether to dismiss.
Onboarding preference stacks
Collect preferences through swipeable intro cards, with the exit animation seeded by the real release velocity.
Flashcard and quiz decks
Create swipe-to-grade study decks for flashcards and quizzes, with keyboard buttons for learners who prefer not to drag.
Velocity-carried motion teaching
Show why an exit should continue from the finger's speed using Popmotion's decay animation, and prototype recommendation feeds the same way.

Got questions?

Frequently Asked Questions

The drag handler recomputes vx (horizontal velocity in px/s) on every pointermove the same way the Popmotion Drag Inertia Card does. On release past the threshold, that vx is passed directly as the velocity option to a decay-type animate() call, so a hard flick continues fast off-screen and a slower drag gets a minimum floor velocity (dir * 900) so it does not crawl.

A drag can cross the threshold slowly -- someone drags deliberately and stops just past 110px with near-zero velocity. Using raw vx in that case would produce a decay animation that barely moves, leaving the card stranded near the threshold instead of exiting. The Math.abs(vx) > 40 ? vx : dir * 900 ternary guarantees the exit always has enough speed to actually leave the screen.

Removing it immediately would cut the exit animation short -- the card would vanish mid-flight instead of visibly leaving the screen. advanceDeck() is only called from the decay animation's onComplete callback, which Popmotion fires once the simulated velocity drops below its rest threshold, guaranteeing the fly-off animation is finished first.

It is computed directly from the current x offset every frame: rotate(pos.x / 18deg). Since pos.x already updates on every pointermove and every decay/spring onUpdate, the rotation is always in sync with position with no extra animation call needed -- it is a derived value, not an independently animated one.

pointerdown calls anim.stop() first if a spring-back animation is still running, cancelling it and letting the new drag take over from wherever the card currently is -- there is no fighting between the in-flight spring and the new pointer-driven position because the spring is explicitly stopped before dragging resumes.

Keep the stack data in component state for rendering the list of people, but drive the top card's live drag position and animation through a ref and imperative style.transform writes exactly as this snippet does -- animating per-frame position through component state would cause excessive re-renders. Call advanceDeck-equivalent state updates (shift the array) only from onComplete, after the exit animation finishes.