This is quite a complicated example. A lot is going on here.
open in CodeSandboxA few details:
- There are always just two cards whose
key
s count up when the first card is removed (changing the key triggers the Animate Presence animation). - The cards have
scale
androtate
Motion values that are transformed by the card’sx
position (when the card is draggable, only the first card is). - The cards are wrapped in an
<AnimatePresence>
, and the first card will have anexit
animation 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:
variantsFrontCard
andvariantsBackCard
. Theexit
variant of the front card uses acustom
property set on the card: thex
position it should animate to. - That
custom
value is saved in anexitX
state and gets set just before theexit
animation 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>
);
}