36. Stack 3D
Another advanced example.
Framer Motion
A few details:
- There are always just two cards, and their
key
s count up when the first one is removed. - The cards are wrapped in an
<AnimatePresence>
, and the first card will have anexit
that animates it to the left or right (starting from the point where you released it). - Each card has
scale
androtate
motion values that are transformed by the card’sx
. - The actual
x
value of theexit
animation is set just before it happens, in thehandleDragEnd()
handler that gets called ononDragEnd()
. - The parent’s
setIndex()
is passed to the first card, and when it is called (in that samehandleDragEnd()
), the cards change position.
function Card(props) {
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,
})
function handleDragEnd(event, info) {
if (info.offset.x < -100) {
props.setExitX(-250)
props.setIndex(props.index + 1)
}
if (info.offset.x > 100) {
props.setExitX(250)
props.setIndex(props.index + 1)
}
}
return (
<motion.div
style={{
width: 150,
height: 150,
position: "absolute",
top: 0,
x: x,
rotate: rotate,
cursor: "grab",
}}
whileTap={{ cursor: "grabbing" }}
drag={props.drag}
dragConstraints={{
top: 0,
right: 0,
bottom: 0,
left: 0,
}}
onDragEnd={handleDragEnd}
initial={props.initial}
animate={props.animate}
transition={props.transition}
exit={{
x: props.exitX,
opacity: 0,
scale: 0.5,
transition: { duration: 0.2 },
}}
>
<motion.div
style={{
width: 150,
height: 150,
backgroundColor: "#fff",
borderRadius: 30,
scale: scale,
}}
/>
</motion.div>
)
}
export function FM36Stack3D() {
const [index, setIndex] = React.useState(0)
const [exitX, setExitX] = React.useState("100%")
return (
<Center>
<motion.div
style={{
width: 150,
height: 150,
position: "relative",
}}
>
<AnimatePresence initial={false}>
<Card
key={index + 1}
initial={{
scale: 0,
y: 105,
opacity: 0,
}}
animate={{
scale: 0.75,
y: 30,
opacity: 0.5,
}}
transition={{
scale: { duration: 0.2 },
opacity: { duration: 0.4 },
}}
/>
<Card
key={index}
animate={{
scale: 1,
y: 0,
opacity: 1,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 20,
opacity: {
duration: 0.2,
},
}}
exitX={exitX}
setExitX={setExitX}
index={index}
setIndex={setIndex}
drag="x"
/>
</AnimatePresence>
</motion.div>
</Center>
)
}
Code Component
Frames instead of Motion Divs, but otherwise the same.
function Card(props) {
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,
})
function handleDragEnd(event, info) {
if (info.offset.x < -100) {
props.setExitX(-250)
props.setIndex(props.index + 1)
}
if (info.offset.x > 100) {
props.setExitX(250)
props.setIndex(props.index + 1)
}
}
return (
<Frame
// Visual & layout
size={150}
backgroundColor="transparent"
// Dragging
drag={props.drag}
dragConstraints={{
top: 0,
right: 0,
bottom: 0,
left: 0,
}}
onDragEnd={handleDragEnd}
// Transformation
x={x}
rotate={rotate}
// Animation
initial={props.initial}
animate={props.animate}
transition={props.transition}
// Animate presence
exit={{
x: props.exitX,
opacity: 0,
scale: 0.5,
transition: { duration: 0.2 },
}}
>
<Frame
// Visual & layout
size={150}
radius={30}
backgroundColor="#fff"
// Transformation
scale={scale}
/>
</Frame>
)
}
export function CC36Stack3D() {
const [index, setIndex] = React.useState(0)
const [exitX, setExitX] = React.useState("100%")
return (
<Frame size={150} backgroundColor="transparent" center>
<AnimatePresence initial={false}>
<Card
key={index + 1}
initial={{
scale: 0,
y: 105,
opacity: 0,
}}
animate={{
scale: 0.75,
y: 30,
opacity: 0.5,
}}
transition={{
scale: { duration: 0.2 },
opacity: { duration: 0.4 },
}}
/>
<Card
key={index}
animate={{
scale: 1,
y: 0,
opacity: 1,
}}
transition={{
type: "spring",
stiffness: 300,
damping: 20,
opacity: {
duration: 0.2,
},
}}
exitX={exitX}
setExitX={setExitX}
index={index}
setIndex={setIndex}
drag="x"
/>
</AnimatePresence>
</Frame>
)
}
Override
This might be possible, probably with three cards that get reused continuously (and no <AnimatePresence>
). But why bother? A component version will always be easier to make.