This is quite a complicated example. A lot is going on here.
A few details:
- There are always just two cards whose
keys count up when the first card is removed (changing the key triggers the Animate Presence animation). - The cards have
scaleandrotateMotion values that are transformed by the card’sxposition (when the card is draggable, only the first card is). - The cards are wrapped in an
<AnimatePresence>, and the first card will have anexitanimation that moves it to the left or right (starting from the point where you release it). - The animations are passed in variants, of which there are two sets:
variantsFrontCardandvariantsBackCard. Theexitvariant of the front card uses acustomproperty set on the card: thexposition it should animate to. - That
customvalue is saved in anexitXstate and gets set just before theexitanimation happens in thehandleDragEnd()handler that is called ononDragEnd(). - The parent’s
setIndex()is passed to the first card, and when it is called (in that samehandleDragEnd()), the cards change position.
This is the Card() component:
function Card(props) {
const [exitX, setExitX] = useState(0);
const x = useMotionValue(0);
const scale = useTransform(x, [-150, 0, 150], [0.5, 1, 0.5]);
const rotate = useTransform(x, [-150, 0, 150], [-45, 0, 45], {
clamp: false
});
const variantsFrontCard = {
animate: { scale: 1, y: 0, opacity: 1 },
exit: (custom) => ({
x: custom,
opacity: 0,
scale: 0.5,
transition: { duration: 0.2 }
})
};
const variantsBackCard = {
initial: { scale: 0, y: 105, opacity: 0 },
animate: { scale: 0.75, y: 30, opacity: 0.5 }
};
function handleDragEnd(_, info) {
if (info.offset.x < -100) {
setExitX(-250);
props.setIndex(props.index + 1);
}
if (info.offset.x > 100) {
setExitX(250);
props.setIndex(props.index + 1);
}
}
return (
<motion.div
style={{
width: 150,
height: 150,
position: "absolute",
top: 0,
x,
rotate,
cursor: "grab"
}}
whileTap={{ cursor: "grabbing" }}
// Dragging
drag={props.drag}
dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
onDragEnd={handleDragEnd}
// Animation
variants={props.frontCard ? variantsFrontCard : variantsBackCard}
initial="initial"
animate="animate"
exit="exit"
custom={exitX}
transition={
props.frontCard
? { type: "spring", stiffness: 300, damping: 20 }
: { scale: { duration: 0.2 }, opacity: { duration: 0.4 } }
}
>
<motion.div
style={{
width: 150,
height: 150,
backgroundColor: "#fff",
borderRadius: 30,
scale
}}
/>
</motion.div>
);
}
And this is the main (exported) component that contains the two cards.
export function Example() {
const [index, setIndex] = useState(0);
return (
<motion.div style={{ width: 150, height: 150, position: "relative" }}>
<AnimatePresence initial={false}>
<Card key={index + 1} frontCard={false} />
<Card
key={index}
frontCard={true}
index={index}
setIndex={setIndex}
drag="x"
/>
</AnimatePresence>
</motion.div>
);
}