Search code examples
reactjsframer-motion

React / Framer Motion is


I'm experimenting with Framer Motion for the first time and trying to create a 3D card effect. The card is supposed to have a dynamic 3D rotation based on mouse movement.

Here's my issue: when the page first loads, the card doesn't render correctly (see screenshot below).

enter image description here

However, once I move my mouse over the card and then move it out (triggering mouseleave), the card snaps to the correct position.

Here's the code I'm using:

import React, { useRef, useEffect } from 'react';
import { motion, useMotionValue, useTransform, useSpring } from 'framer-motion';

const Card3D = () => {
    const cardRef = useRef(null);
    const glareRef = useRef(null);
    const x = useMotionValue(0);
    const y = useMotionValue(0);
    const glareX = useMotionValue(0);
    const glareY = useMotionValue(0);
    const rotateY = useTransform(x, [0, 820], [20, -20])
    const rotateX = useTransform(y, [0, 420], [-20, 20])
    const rotateYSpring = useSpring(rotateY)
    const rotateXSpring = useSpring(rotateX)

    useEffect(() => {
        if (cardRef.current) {
            const cardRect = cardRef.current.getBoundingClientRect();
            x.set(cardRect.width);
            y.set(cardRect.height);
        }
    }, []);

    return (
        <motion.div className='w-[800px] aspect-card2 rounded-32 relative overflow-hidden cursor-pointer' 
            onMouseMove={(e) => {
                if (!cardRef.current || !glareRef.current) return;
                const cardRect = cardRef.current.getBoundingClientRect();
                const glareRect = glareRef.current.getBoundingClientRect();
                x.set(e.clientX - cardRect.left);
                y.set(e.clientY - cardRect.top);
                glareX.set(e.clientX - cardRect.left - glareRect.width / 2);
                glareY.set(e.clientY - cardRect.top - glareRect.height / 2);
            }}
            onMouseLeave={() => {
                if (!cardRef.current || !glareRef.current) return;
                const cardRect = cardRef.current.getBoundingClientRect();
                x.set(cardRect.width / 2);
                y.set(cardRect.height / 2);
                glareX.set(-2000);
                glareY.set(-2000);
            }}
            ref={cardRef}
            style={{
                rotateY: rotateYSpring,
                rotateX: rotateXSpring, 
            }}
        >
            <motion.div className='bg-gradient-radial from-white from-20% to-white/0 to-70% opacity-25 absolute size-[600px]'
                whileHover={{scale: 1.1}}
                style={{
                    x: glareX,
                    y: glareY,
                }}
                ref={glareRef}
            />
            <div className='absolute inset-4 bg-white rounded-32 flex flex-col'>
                <div className='flex-1 border-8 border-gray-300 rounded-32 relative' style={{ backgroundImage: 'url(/progra.jpg)', backgroundSize: 'cover' }}>
                    <img src="/pic1.jpg" alt="Profile picture" className='border-8 border-white rounded-32 absolute left-[3%] top-[3%] w-[15%]' />
                    <div className='bg-slate-800 shadow-[rgba(0,0,0,0.8)_1px_1px_15px] p-4 absolute bottom-0 inset-x-0 text-gray-500 text-xs rounded-2xl translate-y-1/2 flex space-x-3'>
                        <span className='space-x-1 flex items-center'>
                            <b className='text-white text-lg'>Title</b>
                        </span>
                    </div>
                </div>
                <div className='flex-1 text-black rounded-32 px-2 pb-2'>
                    <div className='h-full rounded-32 px-4 shadow-[rgba(0,0,0,0.3)_1px_1px_1px] relative overflow-hidden'>
                        <div className='pt-8'>
                            <h1 className='font-bold text-2xl'>Username</h1>
                            <div className='text-xs space-x-2'>
                                <span>@user</span>
                                <span className='text-gray-400'>Location</span>
                            </div>
                        </div>
                        <hr className='my-4' />
                        <div className='text-xs flex flex-wrap gap-2'>
                            {["#test1", "#test2", "#test3", "#test4", "#test5", "#test6", "#test7"].map((tag) => (
                                <span key={tag} className='border border-black px-2 py-1 rounded-lg'>{tag}</span>
                            ))}
                        </div>
                    </div>
                </div>
            </div>
        </motion.div>
    );
}

export default Card3D;

I tried setting initial values for x and y based on cardRef.current to position the card correctly at the start. I also experimented with different values for these initial positions, but nothing seems to change the initial rendering issue. I expected the card to be in the correct position on load, but it only positions correctly after hovering and then leaving the card.

Does anyone know why the card isn't rendering correctly on initial load and how I can fix this? Any help would be greatly appreciated!

Thanks in advance!


Solution

  • Try this solution: First, set the initial values to the center of the card, assuming a width of 800px and a height of 420px. Then, on mouseleave, also set these values as follows: x.set(400); and y.set(210);`

    import React, { useRef, useEffect } from 'react';
    import { motion, useMotionValue, useTransform, useSpring } from 'framer-motion';
    
    const Card3D = () => {
      const cardRef = useRef(null);
      const glareRef = useRef(null);
    
      // Initial values to center of card assuming width 800px and height 420px
      const x = useMotionValue(400);
      const y = useMotionValue(210);
    
      const glareX = useMotionValue(-2000); // Initially set offscreen
      const glareY = useMotionValue(-2000); // Initially set offscreen
    
      const rotateY = useTransform(x, [0, 800], [20, -20]);
      const rotateX = useTransform(y, [0, 420], [-20, 20]); 
      const rotateYSpring = useSpring(rotateY);
      const rotateXSpring = useSpring(rotateX);
    
      return (
        <motion.div
          className="w-[800px] aspect-card2 rounded-32 relative overflow-hidden cursor-pointer"
          onMouseMove={(e) => {
            if (!cardRef.current || !glareRef.current) return;
    
            const cardRect = cardRef.current.getBoundingClientRect();
            const glareRect = glareRef.current.getBoundingClientRect();
    
            x.set(e.clientX - cardRect.left);
            y.set(e.clientY - cardRect.top);
    
            glareX.set(e.clientX - cardRect.left - glareRect.width / 2);
            glareY.set(e.clientY - cardRect.top - glareRect.height / 2);
          }}
          onMouseLeave={() => {
            if (!cardRef.current || !glareRef.current) return;
    
            // Reset to exact center of the card
            const cardRect = cardRef.current.getBoundingClientRect();
            x.set(400);
            y.set(210);
    
            glareX.set(-2000);
            glareY.set(-2000);
          }}
          ref={cardRef}
          style={{
            rotateY: rotateYSpring,
            rotateX: rotateXSpring,
          }}
        >
          <motion.div
            className="bg-gradient-radial from-white from-20% to-white/0 to-70% opacity-25 absolute size-[600px]"
            whileHover={{ scale: 1.1 }}
            style={{
              x: glareX,
              y: glareY,
            }}
            ref={glareRef}
          />
          <div className="absolute inset-4 bg-white rounded-32 flex flex-col">
            <div
              className="flex-1 border-8 border-gray-300 rounded-32 relative"
              style={{
                backgroundImage: 'url(/progra.jpg)',
                backgroundSize: 'cover',
              }}
            >
              <img
                src="/pic1.jpg"
                alt="Profile picture"
                className="border-8 border-white rounded-32 absolute left-[3%] top-[3%] w-[15%]"
              />
              <div className="bg-slate-800 shadow-[rgba(0,0,0,0.8)_1px_1px_15px] p-4 absolute bottom-0 inset-x-0 text-gray-500 text-xs rounded-2xl translate-y-1/2 flex space-x-3">
                <span className="space-x-1 flex items-center">
                  <b className="text-white text-lg">Title</b>
                </span>
              </div>
            </div>
            <div className="flex-1 text-black rounded-32 px-2 pb-2">
              <div className="h-full rounded-32 px-4 shadow-[rgba(0,0,0,0.3)_1px_1px_1px] relative overflow-hidden">
                <div className="pt-8">
                  <h1 className="font-bold text-2xl">Username</h1>
                  <div className="text-xs space-x-2">
                    <span>@user</span>
                    <span className="text-gray-400">Location</span>
                  </div>
                </div>
                <hr className="my-4" />
                <div className="text-xs flex flex-wrap gap-2">
                  {[
                    '#test1',
                    '#test2',
                    '#test3',
                    '#test4',
                    '#test5',
                    '#test6',
                    '#test7',
                  ].map((tag) => (
                    <span
                      key={tag}
                      className="border border-black px-2 py-1 rounded-lg"
                    >
                      {tag}
                    </span>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </motion.div>
      );
    };
    
    export default Card3D;