Webflow Tutorial

Build an Interactive Parallax Mouse Hover Effect: A Step-by-Step Guide

Blog author Casey Lewis CL Creative
Casey Lewis
November 15, 2024
Build an Interactive Parallax Mouse Hover Effect: A Step-by-Step Guide

In this post, we’ll explore how to create a dynamic parallax effect on hover using HTML, CSS, and JavaScript.

This effect will respond to mouse movements, making elements appear to follow the cursor with subtle shifts, enhancing depth and engagement

Step 1: HTML Structure for Parallax

The HTML structure forms the foundation of the parallax effect by organizing the main image, circle overlay, and other items that will respond to mouse movements.

Assigning specific classes to these elements prepares them for the CSS styling and JavaScript interactions that follow.

Wrapper and Main Image Setup

The hero_animation_wrapper serves as the main container, holding all elements in the parallax animation.

<div class="hero_animation_wrapper">
  <!-- Main Image -->
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/673397a7705e12a6a83fbaa3_Hero%20Image.png>"
    loading="lazy"
    sizes="(max-width: 479px) 100vw, (max-width: 767px) 92vw, (max-width: 991px) 599.984375px, 43vw"
    srcset="
      <https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/673397a7705e12a6a83fbaa3_Hero%20Image-p-500.png> 500w,
      <https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/673397a7705e12a6a83fbaa3_Hero%20Image-p-800.png> 800w,
      <https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/673397a7705e12a6a83fbaa3_Hero%20Image.png>       818w
    "
    alt=""
    class="hero_animation_main-image"
  />
Here is the code in a visual format using Webflow

This code creates the wrapper div and sets the main image with responsive sizes for optimal performance.

Background Circle Layer

The circle overlay, which adds depth to the parallax effect, is positioned behind the main image.

  <!-- Circle Image -->
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/67339861994cabecbc83df55_Circle.svg>"
    loading="lazy"
    alt=""
    class="hero_animation_circle"
  />

This circle element, placed behind the main image, gives the design a layered look, enhancing the visual appeal of the parallax effect.

Parallax Items

The individual parallax items, assigned the .hero_animation_items class, are set to move in response to mouse movements. Each item has an additional class (e.g., is-1, is-2) for custom styling or positioning.

   <!-- Individual images that will move with parallax effect -->
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/6733950ec894756714d2315f_Roles-1.avif>"
    loading="lazy"
    alt=""
    class="hero_animation_items is-1"
    style="transform: translate(0px, 0px)"
  />
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/67339510613d1c39170d52c3_Roles-4.avif>"
    loading="lazy"
    alt=""
    class="hero_animation_items is-2"
    style="transform: translate(0px, 0px)"
  />
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/6733950eb1fce2c3a788302e_Roles-2.avif>"
    loading="lazy"
    alt=""
    class="hero_animation_items is-3"
    style="transform: translate(0px, 0px)"
  />
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/6733950e13b6d12279e9d90d_Roles-5.avif>"
    loading="lazy"
    alt=""
    class="hero_animation_items is-4"
    style="transform: translate(0px, 0px)"
  />
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/6733950e127b15e867c57bc5_Roles-3.avif>"
    loading="lazy"
    alt=""
    class="hero_animation_items is-5"
    style="transform: translate(0px, 0px)"
  />
  <img
    src="<https://cdn.prod.website-files.com/64712356c79d959fca2e97a0/6733950e1bb8c8f294694ed2_Roles-6.avif>"
    loading="lazy"
    alt=""
    class="hero_animation_items is-6"
    style="transform: translate(0px, 0px)"
  />
</div>

Each .hero_animation_items image will respond to the mouse’s movements, creating the parallax effect.

The style="transform: translate(0px, 0px)" attribute initializes each item’s position, setting it up for dynamic transformations in JavaScript.

Step 2: Adding Smooth CSS Transitions

CSS styling enhances each element’s appearance and adds smooth transitions, ensuring graceful movement in response to mouse movements. Absolute positioning for each .hero_animation_items element provides precise control over their placement relative to the main image. Below, we’ll break down each part of the CSS for clarity.

Wrapper and Flexbox Centering

The .hero_animation_wrapper styles center all elements within the wrapper for balanced positioning.

.hero_animation_wrapper {
    justify-content: center;
    align-items: center;
    display: flex;
    position: relative;
}

Circle Element Styling

To create depth, the circle behind the main image is set to absolute position with z-index: 0, aligning it behind the main image. This element is scaled to fit nicely within the wrapper.

.hero_animation_circle {
    z-index: 0;
    object-fit: scale-down;
    align-self: center;
    max-width: 60%;
    position: absolute;
    top: 16%;
    bottom: 0%;
}

General Image Styling

These general styles apply to all images, ensuring they’re responsive, properly aligned, and have a consistent look with rounded corners and no borders.

img {
    max-width: 100%;
    display: inline-block;
    vertical-align: middle;
    border: 0;
    border-radius: var(--size--main-radius);
    box-sizing: border-box;
    overflow-clip-margin: content-box;
    overflow: clip;
}

Main Image Styling

The .hero_animation_main-image style is specifically for the main image (center element), positioned relative to the wrapper and layered above the circle for visual prominence.

.hero_animation_main-image {
    z-index: 2;
    object-fit: fill;
    position: relative;
}

Parallax Item Styling

Each .hero_animation_items element is styled for smooth, contained movement, with absolute positioning and size constraints. The transition property ensures a smooth parallax effect as the mouse moves.

.hero_animation_items {
    object-fit: contain;
    width: 50%; /* Adjusts size relative to wrapper for responsiveness */
    max-height: 80px; /* Keeps images proportional without overflow */
    position: absolute; /* Places each item accurately within the design */
    transition: transform 0.1s ease-out; /* Smooth movement for parallax effect */
}

Specific Item Positioning

Here’s how each .hero_animation_items element is individually positioned within the wrapper. Using position: absolute and adjusting the inset or top and left properties lets each item align accurately around the main image.

.hero_animation_items.is-1 {
    z-index: 1;
    position: absolute;
    inset: 0% 0% auto auto;
}

.hero_animation_items.is-2 {
    z-index: 2;
    position: absolute;
    inset: 36% 0% 0% auto;
}

.hero_animation_items.is-3 {
    z-index: 2;
    position: absolute;
    inset: auto 0% 0% auto;
}

.hero_animation_items.is-4 {
    z-index: 2;
    width: 40%;
    position: absolute;
    inset: auto auto 0% 0%;
}

.hero_animation_items.is-5 {
    z-index: 2;
    position: absolute;
    inset: 51% auto 0% 0%;
}

.hero_animation_items.is-6 {
    position: absolute;
    top: 15%;
    left: -5%;
}

Summary

With these CSS styles, each item’s movement will appear smooth.

Absolute positioning enables precise placement within the wrapper, ensuring each element maintains its place as the parallax effect is applied.

This combination of styles enhances the overall visual quality and depth of the interactive effect.

Step 3: JavaScript for Mouse Tracking and Parallax Logic

This JavaScript code activates the parallax effect by tracking the mouse’s position and applying subtle movements to each item.

By adjusting each item’s transformation based on its position relative to the center of the wrapper, we achieve an interactive, layered movement effect.

1. Initial Setup and Variable Definitions

To begin, we need to select the wrapper and all the items that will be animated.

By storing these elements in variables, we make it easy to apply transformations to each item in response to mouse movements.

// Select the wrapper element and all items to be animated
const wrapper = document.querySelector('.hero_animation_wrapper');
const parallaxItems = document.querySelectorAll('.hero_animation_items');

2. Calculating and Storing Center Coordinates on Mouse Enter

When the mouse enters the wrapper area, we calculate the center coordinates of the wrapper.

These values serve as the anchor point for our effect, making each item’s movement relative to this center.

By storing these coordinates as data attributes on the wrapper, we avoid recalculating them during every mouse movement, which helps maintain a smooth effect.

wrapper.addEventListener('mouseenter', () => {
    // Calculate the center coordinates of the wrapper on mouse enter
    const rect = wrapper.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;

    // Store center coordinates as data attributes for smooth movement calculations
    wrapper.dataset.centerX = centerX;
    wrapper.dataset.centerY = centerY;
});

3. Applying Parallax Effect on Mouse Move

With the mousemove event, we capture the mouse’s position within the wrapper and calculate its offset from the center coordinates.

Each item is then translated based on this offset, with an intensity multiplier that gives each item a unique degree of movement, adding depth to the effect.

This calculation is done for each item independently, creating a layered parallax effect as the mouse moves.

wrapper.addEventListener('mousemove', (e) => {
    // Retrieve the stored center coordinates
    const centerX = parseFloat(wrapper.dataset.centerX);
    const centerY = parseFloat(wrapper.dataset.centerY);

    // Calculate the offset of the mouse position from the center
    const offsetX = (e.clientX - centerX) / 40; // Adjust for desired movement intensity
    const offsetY = (e.clientY - centerY) / 40;

    // Apply the offset to each item, with a unique intensity for depth
    parallaxItems.forEach((item, index) => {
        const intensity = (index + 1) * 2;
        item.style.transform = `translate(${offsetX * intensity}px, ${offsetY * intensity}px)`;
    });
});

4. Resetting Transformations on Mouse Leave

When the mouse leaves the wrapper, this final event listener resets each item’s transformation.

This brings all items back to their starting positions, ensuring a visually clean effect that only activates within the wrapper boundaries.

wrapper.addEventListener('mouseleave', () => {
    // Reset each item’s position to the starting point on mouse leave
    parallaxItems.forEach((item) => {
        item.style.transform = 'translate(0, 0)';
    });
});

Summary of All Steps

By organizing the code into distinct event-driven actions—setting up, calculating the center, tracking movement, and resetting—we create a responsive and user-friendly parallax effect.

Each step contributes to an immersive and visually dynamic interaction.

JavaScript Code in one Block

Here is the JavaScript code in one block so you can copy and paste it easier.


const wrapper = document.querySelector('.hero_animation_wrapper');
const parallaxItems = document.querySelectorAll('.hero_animation_items');

wrapper.addEventListener('mouseenter', () => {
    // Get the bounding rectangle of the wrapper when the mouse enters
    const rect = wrapper.getBoundingClientRect();
    
    // Center coordinates of the wrapper
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;

    // Store these values as data attributes on the wrapper for use during mousemove
    wrapper.dataset.centerX = centerX;
    wrapper.dataset.centerY = centerY;
});

wrapper.addEventListener('mousemove', (e) => {
    // Get stored center coordinates
    const centerX = parseFloat(wrapper.dataset.centerX);
    const centerY = parseFloat(wrapper.dataset.centerY);

    // Calculate offset from the center of the wrapper to the mouse position
    const offsetX = (e.clientX - centerX) / 40; // Adjust divisor for intensity
    const offsetY = (e.clientY - centerY) / 40;

    // Apply the transformation to each item element for a parallax effect
    parallaxItems.forEach((item, index) => {
        const intensity = (index + 1) * 2; // Different intensity for each role
        item.style.transform = `translate(${offsetX * intensity}px, ${offsetY * intensity}px)`;
    });
});

wrapper.addEventListener('mouseleave', () => {
    // Reset transformations when the mouse leaves
    parallaxItems.forEach((item) => {
        item.style.transform = 'translate(0, 0)';
    });
});

How Does This Code Create a Parallax Effect?

The code creates a parallax effect by moving each item element in response to the mouse’s position relative to the center of the wrapper. Here’s how each part contributes to the effect:

  1. Layered Depth:
    • Each item element has a unique intensity multiplier based on its position in the list. This multiplier controls how much each element moves relative to the mouse’s position.
    • Elements with a higher intensity multiplier move more than those with a lower multiplier, creating a layered effect. This mimics how objects closer to a viewer appear to move faster than distant objects, which is the essence of a parallax effect.
    • As a result, the elements appear to be at different “depths,” enhancing the illusion of a 3D space.
  2. Smooth, Responsive Movement:
    • As the mouse moves within the wrapper, small transformations are applied to each item element in real time. The continuous mousemove tracking makes the elements follow the mouse smoothly and responsively.
    • Because the movement is tied to the distance between the mouse and the center of the wrapper, the effect looks natural and centered on the element, regardless of where the wrapper is on the page.
  3. Interactive Feel:
    • The parallax effect is directly responsive to the user’s actions. The elements move in harmony with the mouse, making the page feel interactive and dynamic.
    • This direct response to mouse movement adds a sense of depth and immersion, encouraging user engagement.

Detailed Parallax Code Walkthrough: Controlling and Customizing Intensity

This section dives deeper into how each part of the code contributes to the parallax effect, giving readers an understanding of the intensity calculation, and then explores three different ways to adjust this intensity.

Step-by-Step Breakdown of Key Code Elements

(1) Looping Through Each item Element

This section explains how parallaxItems.forEach() iterates over each item element, passing index to set a unique intensity for each element.

  • parallaxItems.forEach((item, index) => { ... }):
    • parallaxItems is assumed to be a collection (like an array or NodeList) of elements representing each “item” in your setup.
    • .forEach() is an array method that loops over each item in parallaxItems, passing the current item (item) and its position in the array (index) to the callback function.
    • item represents each individual element in parallaxItems, and index is its zero-based position (0, 1, 2, etc.).

(2) Setting Intensity Based on Index

This covers the calculation of intensity and how it determines movement speed for each element, with higher index values resulting in stronger effects.

  • const intensity = (index + 1) * 2;:
    • This line calculates the intensity for each element’s movement based on its position in the array.
    • index + 1 is used to ensure that the lowest intensity starts at 1 rather than 0. If index is 0, intensity becomes 2 (since (0 + 1) * 2 = 2), and it increases for each subsequent element.
    • Multiplying by 2 scales the effect so that each element moves with a progressively stronger intensity.
    • Effect of intensity: Each item element will have a unique intensity, making it move at a different rate than the others, creating a layered or “depth” effect where elements appear to move at varying speeds.

(3) Applying the CSS Transform

This part explains how transform uses offsetX and offsetY to move elements horizontally and vertically in response to the mouse position, creating the parallax effect.

  • item.style.transform = translate(${offsetX * intensity}px, ${offsetY * intensity}px);``:
    • This line applies a CSS transform to each item element, using the calculated offsetX and offsetY values to determine how far to move each element.
    • ${offsetX * intensity}px: The horizontal translation amount. offsetX is multiplied by intensity, so each item element moves a bit more or less than the others, depending on its position in the array.
    • ${offsetY * intensity}px: The vertical translation amount, adjusted similarly by intensity.
    • Result: Each item element is translated by a unique amount based on the mouse’s position relative to the center, creating a parallax effect where elements at different depths respond to the mouse movement with varying intensities.

Fine-Tuning the Intensity of the Parallax Effect

The intensity of the effect is controlled by the divisor value in these lines:

const offsetX = (clientX - centerX) / 40;
const offsetY = (clientY - centerY) / 40;

A higher divisor will make the effect less intense, as it reduces the movement distance for each item element.

Conversely, a lower divisor makes the effect more intense, causing each item element to move more with the mouse.

For example:

  • Less intense effect: Increase the divisor, e.g., offsetX = (clientX - centerX) / 60; which reduces movement.
  • More intense effect: Decrease the divisor, e.g., offsetX = (clientX - centerX) / 10; which increases movement

Example with Less Intensity

Remember, this reduces the movement of the items.

const offsetX = (clientX - centerX) / 60;
const offsetY = (clientY - centerY) / 60;

Adjust this value to find the level of intensity that feels just right for your design.

3 Ways to Control Intensity for Each Element

Depending on your use case, you have several options to set the intensity (how far an element moves). The easiest and least customizable is the DOM order.

If want to set the intensity of each element independent from its order in the DOM, you’ll need to use other methods.

In our case, there are at least two other methods we can use - (1) a class based method or (2) an attribute based method.

We’ll explore all three below as well as look at when you might want to use each.

(1) Using DOM Order for Intensity Control

This method leverages the elements’ order in the DOM. Each item element’s movement intensity is set based on its index in the NodeList. Rearranging elements in the HTML will change their intensity.

When to Use

Simple scenarios where order-based intensity is acceptable.

How does it work?

When you use JavaScript to select elements with a method like document.querySelectorAll or element.getElementsByClassName, it returns a collection (like a NodeList or HTMLCollection) where the order of the elements matches their order in the HTML document (the DOM).

So, for example:

<div class="hero_animation_wrapper">
    <img src="role1.png" class="hero_animation_items">
    <img src="role2.png" class="hero_animation_items">
    <img src="role3.png" class="hero_animation_items">
</div>

If you select all .hero_animation_items elements like this:

const parallaxItems = document.querySelectorAll('.hero_animation_items');

The parallaxItems collection will contain each element in the same order they appear in the HTML:

  • parallaxItems[0] will be the first .hero_animation_items element (role1.png)
  • parallaxItems[1] will be the second .hero_animation_items element (role2.png)
  • parallaxItems[2] will be the third .hero_animation_items element (role3.png)

The index in parallaxItems.forEach((item, index) => {...}) directly reflects this DOM order.

So, if you rearrange the elements in the HTML, their index values will change accordingly, which can alter the intensity of each element’s movement in the parallax effect.

This order-based indexing can be useful if you want specific elements to have different effects based on their positions without having to assign unique classes or attributes.

2. Using Class-Based Intensity

You can assign specific classes to each element to represent different intensity levels, such as intensity-low, intensity-medium, or intensity-high.

When to Use

When you want to control the movement intensity for each element independently and want to do so by assigning specific combo classes to the elements.

How does it work?

Example HTML with Classes

<div class="hero_animation_wrapper">
    <img src="role1.png" class="hero_animation_items intensity-low">
    <img src="role2.png" class="hero_animation_items intensity-medium">
    <img src="role3.png" class="hero_animation_items intensity-high">
</div>

JavaScript Using Class-Based Intensity

In your JavaScript, you can set different intensity values based on the class of each element:

parallaxItems.forEach((item) => {
    let intensity;

    // Set intensity based on the assigned class
    if (item.classList.contains('intensity-low')) {
        intensity = 2;
    } else if (item.classList.contains('intensity-medium')) {
        intensity = 4;
    } else if (item.classList.contains('intensity-high')) {
        intensity = 6;
    } else {
        intensity = 3; // Default intensity if no class is matched
    }

    item.style.transform = `translate(${offsetX * intensity}px, ${offsetY * intensity}px)`;
});

In this example, each element moves with a different intensity based on its assigned class. This setup allows you to control movement by simply adding or changing classes in the HTML.

3. Using Attribute-Based Intensity

Another approach is to use custom data-* attributes to set the intensity directly on each element. This approach allows for more granular control and doesn’t require adding multiple classes.

When to use

When you don’t want to add combo classes and you need more fine-tuned control over the movement of each individual element.

How does it work?

Example HTML with Custom Attributes

<div class="hero_animation_wrapper">
    <img src="role1.png" class="hero_animation_items" data-intensity="1.5">
    <img src="role2.png" class="hero_animation_items" data-intensity="3">
    <img src="role3.png" class="hero_animation_items" data-intensity="5">
</div>

JavaScript Using Attribute-Based Intensity

You can access these custom attributes using JavaScript and use their values directly in the transform calculation:

parallaxItems.forEach((item) => {
    // Get the intensity from the custom attribute, or use a default value
    const intensity = parseFloat(item.getAttribute('data-intensity')) || 2;

    // Apply the parallax effect based on the custom intensity
    item.style.transform = `translate(${offsetX * intensity}px, ${offsetY * intensity}px)`;
});

In this case:

  • data-intensity specifies a unique intensity for each element, making it easy to fine-tune the movement effect.
  • You can control each element’s movement directly by adjusting the data-intensity attribute in the HTML, without needing to modify JavaScript.

Advantages of Using Classes or Attributes

  • Classes: Useful if you want predefined movement categories (e.g., low, medium, high), making it simple to apply consistent effects across multiple elements.
  • Attributes: Ideal for fine-grained control, allowing you to set any intensity level without being restricted to a specific set of classes.

Summary

Using classes or attributes makes it easy to control each element’s movement independently, regardless of its DOM position.

This setup is more flexible and scalable, especially when you want to customize the behavior of individual elements without modifying the JavaScript logic extensively.

Claim Your Design Spot Today

We dedicate our full attention and expertise to a select few projects each month, ensuring personalized service and results.

Web design portfolio