Animation » Example Animations » 32. Animate Presence: Stack 3D

32. Animate Presence: Stack 3D

This is quite a complicated example. A lot is going on here.

Code component

A few details:

  • There are always just two cards, and their keys count up when the first card is removed (changing the key triggers the Animate Presence animation).
  • The cards are wrapped in an <AnimatePresence>, and the first card will have an exit animation that moves it to the left or right (starting from the point where you released it).
  • The cards have scale and rotate MotionValues that are transformed by the card’s x position.
  • The actual x value of a card’s exit animation gets set just before it happens, in the handleDragEnd() handler that is 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 default function CC_32_Animate_Presence_Stack_3D(props) {
    const [index, setIndex] = useState(0)
    const [exitX, setExitX] = useState("100%")

    return (
        <div>
            <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>
        </div>
    )
}

The version in the CodeSandbox is still in React 17 because, in React 18, the drag direction (left or right) gets lost. This can probably be fixed by using variants with the custom property instead, so that we can also pass this custom value to the <AnimatePresence> component.

Code 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.


6 comments on “32. Animate Presence: 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 .

      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’.

  • cookie1qsh says:

    Hi, I’m trying to add another framer in the default function that the color could effect by the motionvalue when the card swiping (kinda like the Example ’24. Colors: Interpolation’), My code is attached below, but it doesn’t work at all, seems the children’s value can’t be transport into the parent.

    • Tes Mat says:

      It’s hard to debug standalone code, because I can’t make changes and see what happens.
      Do you have a link to a project? (You can mail it if you prefer. Link in FAQ)

  • lukevakasisikakala says:

    how would i go about adding more cards

    • Tes Mat says:

      Something like this:

                  <AnimatePresence initial={false}>
                      <Card
                          key={index + 2}
                          initial={{ scale: 0, y: 105, opacity: 0 }}
                          animate={{ scale: 0.5, y: 60, opacity: 0.4 }}
                          transition={{
                              scale: { duration: 0.2 },
                              opacity: { duration: 0.4 }
                          }}
                      />
                      <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
                      …

      Here’s that project on CodeSandbox, but you should be able to copy/paste that added code to the Framer project.

      I added one card, gave it a key of index + 2, and also tweaked the animate values (smaller scale and opacity + lower y position). Because there are now three cards you can see through the second one, so I would tweak the backgroundColor instead of using opacity.

Leave a Reply