Building a Flip Cards Component
A while back I spotted this gorgeous flip cards component in Framer's marketplace — stacked cards you could drag, flip through with physics-like reordering, and just the right amount of bounce from the spring animation. I loved it and immediately wanted to recreate something like that using Motion. I mean, I purchased Emil's course, so I was eager to try something fun by getting my hands dirty.
What I thought would be a weekend project turned into a multi-weekend journey through prototyping, production disasters, and eventually shipping something I'm really happy with. If you've ever had animations work perfectly in dev only to completely break in production, this one's for you.
Here’s the play-by-play.
Building the Initial Stack & Basic Drag
Started simple: positioned each of the cards properly to stack together paired with the right amount of rotation to create the feel of a deck of cards. I made it dynamic just so that regardless of the number of cards they will each be positioned properly and rotated well.
The drag part? Believe me, it was simpler than I thought. I only needed to add the drag prop and Framer Motion did the magic.
It already felt promising:
But it was way too loose. Cards flew off-screen with no boundaries, no snap-back, and no real "deck" behavior.
Adding Drag Constraints for Control
First real improvement: limited the drag so the cards stayed within reasonable bounds.
Much better control:
Adding Snap-back + Springiness
Next I added dragSnapToOrigin which later caused me problems but at the time, it did all i wanted it to. When you release a card it snaps back to it’s original position, plus a springy feel. I also restricted the drag to only the card that sits on the top of the stack.
The bounce gave it personality, though I’ll admit I overdid the spring at first—it felt almost cartoonish:
At this point, everything was working beautifully in development. Little did I know what awaited me in production.
Making the Stack Reorder on Flip
Next I worked on the drag to snap back and reorder the stack which was a bit confusing at first if I'm being honest, but after thinking through it and so many trials, I got it working and to my surprise it was simpler than I thought. Apparently I missed the very basics of React, lol.
setCards((prev) => {
const [first, ...rest] = prev;
return [...rest, first];
});The Production Nightmare
I had the mechanics mostly working. Positioning during the flip animation looked perfect—cards rotated cleanly, stack reordered without jumps.
Then I built for production.
…and everything broke during the flip.
The top card would rotate, but the cards underneath (or the whole stack) would suddenly jump to wrong positions mid-animation. Looked fine on localhost, completely mangled in prod.
I posted about it in the animations on the web discord channel wondering if anyone else had hit this exact dev/prod flip positioning desync.
Classic “works on my machine” moment.
Let’s buckle up it’s about to get a bit too technical.
Someone replied and made reference to the issue most likely stemming from the transform, stating it had probably lost its translateX and translateY after the drag. While I wasn't using the transform property to position the cards, it brought my attention to it.
The Aha Moment
After comparing the dev and production behaviors, I started examining how Framer Motion handles drag animations. Here's what the original (broken) code looked like:
<motion.img
src={image.url}
style={{
top: "50%",
left: "50%",
zIndex: totalCards - index,
}}
initial={{
rotate: rotation,
x: "-50%",
y: "-50%",
}}
animate={{
rotate: rotation,
x: "-50%", // The problem starts here
y: "-50%", // And here
}}
drag={isTopCard}
dragConstraints={constraintRef}
dragSnapToOrigin // This doesn't work as expected
onDragEnd={handleDragEnd}
/>At first glance, this looks reasonable. The animate prop centers the card using x: "-50%" and y: "-50%", while dragSnapToOrigin should return the card to its starting position after dragging.
But there's a fundamental conflict happening under the hood:
Here's what's actually happening frame-by-frame:
When you drag the card, Framer Motion updates the internal
xandymotion valuesWhen you release,
dragSnapToOrigintries to animate back to position(0, 0)But the
animateprop is simultaneously trying to enforcex: "-50%"andy: "-50%"These two animation directives fight each other, creating a race condition
But there's a fundamental conflict happening under the hood:
In development, the hot module reloading and slower execution masked this timing issue. But in production, with optimized and minified code, the race condition became consistent and visible.
The Fix
The fix required rethinking how to separate static positioning from animated drag movement. Here's the corrected approach:
const x = useMotionValue(0);
const y = useMotionValue(0);
const handleDragEnd = (_: any, info: PanInfo) => {
if (onDismiss) {
onDismiss();
x.set(0); // Explicitly reset to center
y.set(0);
}
};
return (
<motion.div
className="absolute top-1/2 left-1/2 -translate-1/2" // CSS handles centering
style={{
x, // Motion value for drag
y,
zIndex: totalCards - index,
touchAction: isTopCard ? "none" : "auto",
}}
animate={{
rotate: rotation, // Only animate rotation
}}
drag={isTopCard}
dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
dragElastic={0.5}
onDragEnd={isTopCard ? handleDragEnd : undefined}
>
<img src={card.url} alt={`Card ${card.id}`} />
</motion.div>
);The key changes:
The explicit motion values (
xandy) track the drag offset from the center point. They start at 0 (centered) and update during drag.Rather than relying on
dragSnapToOrigin(which conflicts with theanimateprop), I manually reset the motion values inhandleDragEnd. This gives me complete control over the snap-back behavior.CSS handles the static centering, while motion values handle the drag offset.
Once fixed, I took my time to play around with it till it got to the level i was satisfied with it
You can play around with the final component in my playground.