DWC
July 28th 2024

How I created an interactive 3-D parallax effect in React

5 min

2024 has seen the rise of interactivity in web design. Designers are pushing the boundaries with 3-D elements to give users a unique experience. Parallax effects with layers in the foreground and background moving at different speeds to create a realistic perspective are paticularly popular. In this article, I'll break down how I implemented a parallax scroll effect using the mouse position in React.js with Javascript and CSS.

The first and most important decision is to choose a scene. Find a high-resolution image that has layers at varying distances from the point of observation. That means there should be layers in the foreground and in the background of the image, otherwise it will be impossible to create the illusion of depth.

scene img

Once we have the image, we can move into Photoshop or another photo editing tool of your choice. This isn't a photoshop tutorial so I will keep the explanation brief. In my case, the image needed color adjustments to dampen the whites so the text content would be able to stand out later on. The main work involved is cutting out each individual layer and generating a filling for the cut out sections.

Import the layers into React by adding them into the public folder of the root directory. Using CSS, give them an absolute position and tweak their top/left values until they are placed in the correct position on the page. Giving each layer a width of 100% will ensure they are the right size. The layers in the foreground need a higher z-index value so they sit at the top of the stack. Each <img> element has the parallax class so that we can translate the x, y and z coordinates later with a Javascript function. The parallax class is given a scale property so that the scene is slightly zoomed in. This to stop the edge of the background layers from becoming visible after they are shifted due to the parallax effect. The transition property gives the movement a smooth feel.

.parallax {
    transform: translate(-50%, -50%);
    scale: 1.085;
    transition: 0.45s cubic-bezier(0.2, 0.49, 0.32, .99);
}

.sky {
    position: absolute;
    width: 100%;
    top: 28%;
    left: 54.25%;
    z-index: 0;
}
.sea {
    position: absolute;
    width: 100%;
    top: 89%;
    left: 54.03%;
    z-index: 1;
}

Before we can begin implementing the parallax effect logic, we need to attach a dataset to each layer. This will allow us to create relative motion between the foreground and background layers when the mouse moves. Layers in the background should generally have higher values for speedX & speedY than the foreground layers but lower values for speedZ and rotation. You will need to tweak the values later on when you can actually see the effect so don't worry about them too much now.


{/* Background layer example */}
<img
    src="/hero/sky6.avif"
    alt="sky"
    className="sky parallax"
    data-speedx="0.08"
    data-speedy="0.075"
    data-speedz="0"
    data-rotation="0"
/>

{/* Foreground layer example */}
<img
    src="/hero/left-cliff.png"
    alt="left cliff"
    className="left-cliff parallax"
    data-speedx="0.05"
    data-speedy="0.045"
    data-speedz="0.6"
    data-rotation="0.15"
/>
    

Finally, we can derive the logic for the mousemove function. The basic idea is to determine the position of the mouse relative to the center of the screen and displace the layers according to their dataset values using a transform inside a for loop. We use a isInLeft var to check which side of the page the layer is on. Layers will rotate clockwise or anti-clockwise around the y-axis depending the boolean value.

function handleMousemove(e) {
    // Determine the x coordinate of the mouse relative to the center of the screen
    const xValue = e.clientX - window.innerWidth / 2;

    // Determine the y coordinate of the mouse relative to the center of the screen
    const yValue = e.clientY - window.innerHeight / 2;

    // Calculate the degree of rotation based on the x coordinate value
    const rotateDegree = (xValue / (window.innerWidth / 2)) * 25;

    // Get all elements with the class 'parallax'
    const parallaxElements = Array.from(document.getElementsByClassName('parallax'));

    // Iterate over each parallax element
    parallaxElements.forEach((el) => {
        // Destructure the dataset attributes (speedx, speedy, speedz, rotation) from the element
        const { speedx, speedy, speedz, rotation } = el.dataset;

        // Determine if the element is on the left side of the screen
        let isInLeft = parseFloat(getComputedStyle(el).left) < window.innerWidth / 2 ? 1 : -1;

        // Calculate the z-axis value based on the mouse x coordinate and element's left position
        let zValue = (e.clientX - parseFloat(getComputedStyle(el).left)) * isInLeft * 0.06;

        // Apply the transform to the element
        el.style.transform = `translateX(calc(-50% + ${-xValue * parseFloat(speedx)}px)) translateY(calc(-50% + ${yValue * parseFloat(speedy)}px))  translateZ(${zValue * parseFloat(speedz)}px) rotateY(${rotateDegree * parseFloat(rotation)}deg)`;
    });
}

Use a loadEvents function to attach the mousemove event listener to the parallax layers upon component mounting. This function checks whether the window object is defined or not to avoid errors in React.

function loadEvents() {
    if (typeof window !== 'undefined') {
        window.addEventListener('mousemove', handleMousemove);
    }

    // Cleanup function to remove event listener when the component unmounts
    return () => {
        if (typeof window !== 'undefined') {
            window.removeEventListener('mousemove', handleMousemove);
        }
    };
}

loadEvents();

You should be all set now! Feel free to reach out through my Contact form if you have any questions. You can find the full repo for the project here.

© 2024 Douglas William Carton. Portfolio custom built in Next.js, Three.js and Framer Motion.