Source Code

<div class="wrap">
  <div class="stack" id="stack">
    <div class="card" style="--bg: linear-gradient(140deg,#6366f1,#818cf8)">
      <h3>Mountain Trail</h3>
      <p>Colorado, USA</p>
    </div>
    <div class="card" style="--bg: linear-gradient(140deg,#06b6d4,#22d3ee)">
      <h3>Coastal Cliffs</h3>
      <p>Big Sur, USA</p>
    </div>
    <div class="card" style="--bg: linear-gradient(140deg,#f59e0b,#fbbf24)">
      <h3>Desert Dunes</h3>
      <p>Sahara, Morocco</p>
    </div>
    <div class="card" style="--bg: linear-gradient(140deg,#ec4899,#f472b6)">
      <h3>Northern Lights</h3>
      <p>Tromso, Norway</p>
    </div>
    <div class="card" style="--bg: linear-gradient(140deg,#10b981,#34d399)">
      <h3>Rainforest Canopy</h3>
      <p>Borneo, Malaysia</p>
    </div>
  </div>

  <div class="actions">
    <button class="act skip" id="skipBtn" aria-label="Skip">&times;</button>
    <button class="act like" id="likeBtn" aria-label="Like">&hearts;</button>
  </div>
  <p class="hint">Drag a card left or right, or use the buttons.</p>
</div>

3D Swipe Card Stack — Tinder-Style CSS JS

3D Swipe Card Stack · Animations · Plain HTML, CSS & JS · Live preview

What's included

Features

Real 3D stacking via translateZ + rotateX inside a perspective container, not a 2D scale illusion
Pointer Events drag (pointerdown/pointermove/pointerup) unifies mouse, touch, and pen input
Live rotateZ/rotateY tilt proportional to drag distance while the top card is being dragged
Fly-away swipe animates translateZ, rotateY, and rotateZ together for a genuine tumble-away exit
Distance-threshold logic decides swipe vs. snap-back on pointer release
Like/skip buttons call the identical resolveSwipe() function used by drag, so both stay in sync
layoutStack() re-derives every card position purely from its index — add/remove cards freely
Empty-state message appears automatically once every card has been swiped
Zero dependencies — no Hammer.js, no swipe-gesture library
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

3D Swipe Card Stack — Real translateZ Depth and a rotateY Fly-Away Swipe

Screenshot of the 3D Swipe Card Stack snippet rendered live

Swipeable card decks are everywhere — dating apps, recommendation feeds, onboarding "pick your interests" flows. Most web implementations fake the stacked look with 2D scale and a vertical offset. This one goes further and gives the deck genuine 3D depth, and animates the swiped card away with real translateZ and rotateY, so it visibly recedes and tumbles rather than just sliding sideways.

Real 3D stacking, not a 2D illusion

layoutStack() positions every card behind the top one with transform: translate3d(0, depth*8px, depth*-30px) rotateX(depth*-2deg) scale(1 - depth*0.045). The translateZ component (the third value in translate3d) pushes each successive card back along the Z axis inside the .stack container's perspective: 1200px, so cards further back genuinely recede into the screen rather than merely appearing smaller. Adding a small rotateX tilt on top gives the deck a subtle fanned-open feel instead of a perfectly flat stack.

Dragging the top card

bindTopCard() attaches pointerdown/pointermove/pointerup listeners to whichever card is currently on top (the Pointer Events API unifies mouse, touch, and pen input in one set of events). While dragging, the card's transform follows the pointer's horizontal and vertical delta directly, plus a rotateZ twist and a rotateY 3D tilt both proportional to horizontal drag distance — so the card doesn't just translate, it visibly banks like a card being flicked off a real deck.

Deciding the swipe outcome

On pointer release, onUp() checks the accumulated horizontal delta against a 110px threshold in either direction. Past the threshold, resolveSwipe(card, dir) takes over and adds .fly-left or .fly-right, which animates the card to translate3d(±460px, 40px, -60px) rotateY(±35deg) rotateZ(±24deg) with opacity: 0 — the card doesn't just leave sideways, it tumbles away in 3D and recedes in depth at the same time, driven by the same cubic-bezier(0.34, 1.56, 0.64, 1) overshoot easing used for the resting stack transforms. Below the threshold, the card's inline transform is simply cleared and layoutStack() restores it to its resting position in the deck.

Removing the card and re-stacking

resolveSwipe() listens for transitionend on the flying card, removes it from the DOM once the fly-away animation finishes, then calls layoutStack() again (so every remaining card shifts up one position in depth) and bindTopCard() again (so drag listeners are re-attached to whatever card is now on top). Re-binding on every card change, rather than delegating listeners once at the container level, keeps the drag logic simple since each card's own local dx/dy/dragging state never has to be reset or looked up by identity.

Buttons as an alternative input

The like/skip buttons call the exact same resolveSwipe() function the drag gesture uses, just with a hardcoded direction instead of one derived from pointer position — this keeps swipe-by-drag and swipe-by-button visually and behaviorally identical, and means any future change to the fly-away animation automatically applies to both input methods.

Adapting the deck

Because every card's resting transform is computed purely from its index (depth) inside layoutStack(), adding, removing, or reordering cards in the HTML requires no other code changes — the function re-derives every card's position, rotation, scale, and opacity from scratch on each call.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet into an AI coding assistant like Claude and ask it to walk through how translateZ combines with the parent's perspective to create real depth in the stack, versus just scaling cards smaller in 2D — understanding that distinction is what lets you confidently retune the stacking depth values. It is also a great base to extend: ask the assistant to help you add velocity-based swipe detection (using the drag speed, not just distance, to decide a swipe) or to persist which cards have already been swiped in localStorage so the deck resumes where the user left off.

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 swipeable, Tinder-style card stack in plain HTML, CSS, and JavaScript with genuine 3D depth between stacked cards and a 3D fly-away swipe animation — no gesture library.

Requirements:
- A stack of absolutely-positioned card elements inside a container with CSS perspective set on the container.
- A function that positions every card except the top one using translate3d with a Z-axis offset proportional to its stack index (so cards further back genuinely recede in 3D, not just scale down), plus a small rotateX tilt and a slight downward Y offset, so it reads as a fanned deck.
- Pointer Events (pointerdown/pointermove/pointerup) drag support on the top card: while dragging, the card should follow the pointer's X/Y movement and rotate proportionally (rotateZ and a 3D rotateY tilt) based on horizontal drag distance.
- On release, if the horizontal drag distance passed a threshold (around 100-120px) in either direction, animate the card flying off screen in that direction using translate3d (including a further negative Z push so it recedes as it exits), rotateY, and rotateZ together, fading its opacity to 0, then remove it from the DOM once the transition ends and re-layout the remaining stack.
- If the drag did not pass the threshold, snap the card back to its resting stacked position instead.
- Add like and skip buttons that trigger the exact same swipe-resolution function as dragging, just with a fixed direction, so both input methods behave identically.
- Show a simple message once every card has been removed from the stack.

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.

Step by step

How to Use

  1. 1
    Drag the top cardDrag left or right past roughly 110px and release — it flies away in 3D and reveals the next card.
  2. 2
    Use the buttonsClick the X or heart button to skip or like the top card without dragging.
  3. 3
    Add more cardsAdd more .card elements inside #stack in the HTML panel — layoutStack() automatically re-derives every position from index.
  4. 4
    Adjust stack depth/spacingChange the depth*8/depth*-30/depth*-2/depth*0.045 multipliers inside layoutStack().
  5. 5
    Change the swipe thresholdEdit the 110 (pixels) comparison inside onUp() to make swipes trigger sooner or require more drag distance.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Recommendation and discovery feeds
The canonical use case — swipe-to-decide interfaces for matching, browsing, or curating a list of items one at a time.
Onboarding preference pickers
Ask new users to like/skip through a stack of interests, topics, or products to seed a personalization model.
Flashcard-style review decks
Swipe left for "review again" and right for "got it" through a stack of study or vocabulary cards.
Learn 3D drag interactions
A compact reference for combining Pointer Events drag tracking with CSS 3D transforms (translateZ, rotateY, rotateX) instead of flat 2D dragging.
Portfolio/case-study card decks
Present project highlights or testimonials as a swipeable deck with real depth instead of a flat carousel.
Keyboard-alternative browsing
The like/skip buttons already provide a non-drag path through the deck; extend with keyboard arrow-key bindings for full accessibility.
Related: Avatar Stack Fan Expand
See the Avatar Stack Fan Expand for a related animations pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

It is real 3D. Each card behind the top one gets a translateZ offset (pushing it back along the Z axis) inside a parent with CSS perspective, plus a small rotateX tilt, so the deck has genuine perspective falloff rather than just being scaled smaller in 2D.

onUp() compares the total horizontal drag distance (dx) against a 110px threshold in either direction. Past the threshold, resolveSwipe() runs the fly-away animation. Below it, the card transform resets and layoutStack() restores its resting position.

The Pointer Events API (pointerdown/pointermove/pointerup) fires for mouse, touch, and pen input through one unified event model, so a single set of listeners handles dragging on both desktop and mobile without duplicating logic for each input type.

Both paths call the exact same resolveSwipe(card, direction) function — the buttons just hardcode the direction instead of deriving it from drag distance — so any change to the fly-away animation automatically applies to both.

Add more .card elements inside #stack in the HTML. layoutStack() computes every card resting transform purely from its position in the DOM, so no JavaScript changes are needed when the card count changes.

resolveSwipe() checks stack.children.length after removing a card, and if none remain, appends a plain "You have viewed every card" message in place of the deck.