Framer » Animation » Example Animations » 36. Stack 3D

36. Stack 3D

Another advanced example.

Framer Motion

A few details:

  • There are always just two cards, and their keys count up when the first one is removed.
  • The cards are wrapped in an <AnimatePresence>, and the first card will have an exit that animates it to the left or right (starting from the point where you released it).
  • Each card has scale and rotate motion values that are transformed by the card’s x.
  • The actual x value of the exit animation is set just before it happens, in the handleDragEnd() handler that gets called on onDragEnd().
  • The parent’s setIndex() is passed to the first card, and when it is called (in that same handleDragEnd()), 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.


2 comments on “36. Stack 3D”

  • shuyang says:

    After dragging out the top card, is both cards’ key changed from 0, 1 to 1, 2? I think I get the idea of what is going when dragging the top card, but after drag, why is the bottom card animating to the same size as top card? Are you planning doing a break down for this example soon?

    • Tes Mat says:

      I didn’t make this one, and I also had to take a good look to figure out what’s happening (I only did the code component version).

      Yes, the index (and therefore the cards’ keys) always counts up.

      I suppose the bottom card animates to the front because it becomes that card by virtue of its key.

      Let’s say the bottom card at some point has a key of 6, and the front one a key of 7. When the index counts up, the bottom card becomes the one with a key of 7 and, therefore, kind of ‘assumes that position’.

Leave a Reply