What We're Building
- A stack of photo cards with depth (scale + offset so you can feel the pile)
- Drag on the top card, with rotation tied to horizontal movement
- A swipe threshold—drag far enough and the card flies off
- Cards re-enter at the back of the stack after being swiped
No external state machines. No reducers. Just motion and useState.
Setup
You'll need Motion for React. If you're on the older framer-motion package, the API is identical—swap the import.
We'll also use clsx for conditional classes, though you can inline styles if you prefer.
The Data
Let's define a simple array of photo cards. Swap in your own images or wire this up to an API—the component doesn't care.
The Card Component
Each card is a motion.div that only accepts drag when it's on top. The key mechanic: we use useMotionValue and useTransform to derive rotation from the x position. This is the part that makes the interaction feel physical rather than programmatic.
A few things worth calling out:
dragConstraints={{ left: 0, right: 0 }} + dragElastic={0.85} — Setting constraints to zero and elastic to a high value gives you that rubbery feel. The card lags behind your finger slightly, then snaps back if released early.
dragSnapToOrigin — When the user releases below the swipe threshold, the card snaps cleanly home. No manual spring animation needed.
exit on the parent AnimatePresence — The exit animation reads the current x value to decide which direction to throw the card. This is the moment it flies off screen.
The Stack Container
The stack manages the order. When a swipe fires, we shift the top card to the bottom of the array—so nothing is ever truly deleted.
Why slice(0, 3)? Rendering only the top 3 cards keeps the DOM lean. The user can never see deeper than 2–3 cards in the stack anyway, so there's no reason to mount all five (or fifty).
AnimatePresence initial={false} — The initial={false} prop prevents the enter animation from firing on first render. Without it, every card would animate in when the page loads.