Some Examples » 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 whose keys count up when the first card is removed (changing the key triggers the Animate Presence animation).
  • The cards have scale and rotate Motion values that are transformed by the card’s x position (when the card is draggable, only the first card is).
  • 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 release it).
  • The animations are passed in variants, of which there are two sets: variantsFrontCard and variantsBackCard. The exit variant of the front card uses a custom property set on the card: the x position it should animate to.
  • That custom value is saved in an exitX state and gets set just before the exit animation 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.

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 }),
    }
    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,
            }}
            drag={props.drag}
            dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
            onDragEnd={handleDragEnd}
            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 component that contains the two cards.

export default function CC_32_Animate_Presence_Stack_3D(props) {
    const [index, setIndex] = useState(0)

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

Code override

This might be possible with three cards that get reused continuously (and no <AnimatePresence>). But why bother? A component version will always be easier to make.


Join the Mighty Guides mailing list    ( ± 6 emails/year )

GDPR

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing per their Privacy Policy and Terms.



Leave a Reply