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

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

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

This snippet combines several Popmotion techniques (drag tracking, decay, spring, threshold logic) into one interaction, so it's a good candidate for a guided walkthrough with an AI assistant. Paste the code into Claude and ask it to trace the full lifecycle of a single swipe from pointerdown to the next card being promoted, identifying exactly where the threshold decision happens and why onComplete rather than pointerup is what triggers advanceDeck(). Then ask what would go wrong if the velocity floor (dir * 900) were removed entirely -- slow deliberate drags past the threshold would produce a decay animation too weak to actually clear the viewport, leaving a dismissed card visibly stuck near the edge. To extend it: ask it to add a third dismissal direction (swipe up to "super-like"), add haptic-style scale feedback proportional to drag distance, generalize the deck to load more cards lazily as the stack thins, or add a subtle rotation-based shadow that intensifies as the stamp opacity increases.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Tinder-style swipeable card stack using Popmotion v8 (from a CDN, global object popmotion) in plain HTML, CSS, and JavaScript.

Requirements:
- Render a stack of at least 5 cards absolutely positioned on top of each other, each showing a name/subtitle over a gradient background. Cards behind the top one are visually offset with a slight vertical translateY and scale reduction so the stack reads as a deck, with z-index descending by position.
- Only the frontmost card is draggable, using Pointer Events (pointerdown/pointermove/pointerup) with setPointerCapture. While dragging, apply translate(x, y) plus a rotation proportional to horizontal offset (e.g. x / 18 degrees) so the card tilts like it is pivoting from a thumb.
- Track horizontal pointer velocity on every pointermove the same way a drag-inertia card would: (change in clientX) / (change in time) scaled to px/s.
- Show a "Like" stamp that fades in when dragging right and a "Nope" stamp that fades in when dragging left, with opacity proportional to how close the drag is to a fixed pixel threshold (e.g. 110px), reaching full opacity exactly at the threshold.
- On release: if the drag distance exceeds the threshold, animate the card off-screen using popmotion.animate({ type: 'decay', velocity: <the tracked release velocity, with a sane minimum floor so slow-but-past-threshold drags still exit fully>, power, timeConstant, onUpdate, onComplete }). Only once onComplete fires, remove the card from the DOM/data array and promote the next card into the top slot with a small spring animation on its scale/offset. If the drag distance does NOT exceed the threshold, spring the card back to its original position and rotation using type: 'spring'.
- Add Like/Pass buttons below the deck that trigger the identical decay-based exit programmatically (as if the user had swiped), plus a Reset button that rebuilds the full deck.
- Show a dashed-border empty state once all cards are dismissed.
- Style it as a dark themed deck with rounded cards, a bottom gradient overlay for text legibility, and pill-shaped action buttons.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<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>

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 UIs
The canonical Tinder-style swipe deck for people, listings, or recommendations.
Review and triage queues
Approve/reject flows for photos, submissions, or moderation items.
Onboarding card stacks
Swipeable intro or preference cards during first-run setup.
Flashcard and quiz apps
Swipe-to-grade study decks with know/don't-know gestures.
Teaching velocity-carried motion
A concrete example of decay animation seeded with real pointer velocity.
Recommendation feed prototypes
Quick prototyping of swipe-based content feeds before native implementation.

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.