Search

29. Animate Presence: Stack 3D

29. Animate Presence: Stack 3D

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

⚡️ Some Examples – 29 – Animate Presence: Stack 3D
open in CodeSandbox

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,
            transition: { duration: 0.2 }
        })
    };
    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,
                cursor: "grab"
            }}
            whileTap={{ cursor: "grabbing" }}
            // Dragging
            drag={props.drag}
            dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
            onDragEnd={handleDragEnd}
            // Animation
            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 (exported) component that contains the two cards.

export function Example() {
    const [index, setIndex] = useState(0);

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

Leave a Reply

plugins premium WordPress
Scroll to Top