Framer X » Animation » Example animations » 36. Stack 3D

36. Stack 3D

Another advanced example.

Open Framer Motion version in CodeSandbox

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.


Leave a Reply