Thank you for your interest and welcome!

We’ve just sent you an email! Check your inbox (and spam folder) and save our email so you don’t miss what’s next!

Meanwhile, rendez-vous here:

GSAP x Made With Gsap
Inertia Plugin

Big news: GSAP plugins are now free! To celebrate, here’s a demo that makes use of the InertiaPlugin. In this effect, images move when hovered. The hovered image shifts in the direction of the mouse movement, with an intensity based on the speed of that movement. Let’s begin!

HTML Structure

The HTML structure for this effect is fairly straightforward. All cards are placed inside a container that’s centered both horizontally and vertically.

<div class="medias">
    <div class="media">
        <img src="./assets/medias/01.png" alt="">
    </div>
    <div class="media">
        <img src="./assets/medias/02.png" alt="">
    </div>
    <div class="media">
        <img src="./assets/medias/03.png" alt="">
    </div>
    ...
</div>

Some CSS

We’ll arrange the cards side by side in rows of 4 using the display: grid property.

.mwg_effect000 .medias {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1vw;
}

Each image keeps a square ratio. To optimize its transformations, I use the will-change: transform property. This tells the browser that this property will change frequently throughout the effect.

.mwg_effect000 .medias img {
    width: 11vw;
    height: 11vw;
    object-fit: contain;
    border-radius: 4%;
    pointer-events: none;
    will-change: transform;
}

Movement delta

Now let’s calculate the movement delta of the mouse. For this part, I start by creating a mousemove event that triggers as soon as the user moves its mouse.
Calculating the delta is quite simple: we just store the previous x and y values in a variable, then subtract them from the current values.

let oldX = 0, 
    oldY = 0, 
    deltaX = 0,
    deltaY = 0

const root = document.querySelector('.mwg_effect000')
root.addEventListener("mousemove", (e) => {
    // Calculate horizontal movement since the last mouse position
    deltaX = e.clientX - oldX;
    
    // Calculate vertical movement since the last mouse position
    deltaY = e.clientY - oldY;

    // Update old coordinates with the current mouse position
    oldIncrX = e.clientX;
    oldIncrY = e.clientY;
})

Using Inertia

Now for the most exciting part of the effect: we’re going to trigger inertia on the x and y axes when the user hovers over an image. To do that, we first declare a mouseenter event for each image in the effect.

root.querySelectorAll('.medias div').forEach(el => {
    // Add an event listener for when the mouse enters each media
    el.addEventListener('mouseenter', () => {
        // The magic happens here
    })
})

Each time the event is triggered, we create a gsap.timeline(). This timeline will play a sequence of tweens. For performance reasons, each timeline is killed as soon as it’s done playing.

root.querySelectorAll('.medias div').forEach(el => {
    el.addEventListener('mouseenter', () => {
        const tl = gsap.timeline({ 
            onComplete: () => {
                tl.kill()
            }
        })
        tl.timeScale(1.2) // Animation will play 20% faster than normal
    })
})

The first tween in the timeline uses GSAP’s InertiaPlugin. By retrieving the deltaX and deltaY values from our mousemove event, we apply a transformation to the hovered image using the velocity property. You’ll notice we’re using the end property as well. By setting it to 0, we define that the motion should end right where it started—so the image smoothly returns to its initial position once the animation completes.

root.querySelectorAll('.medias div').forEach(el => {
    el.addEventListener('mouseenter', () => {
        ...

        const media = el.querySelector('img')
        tl.to(media, {
           inertia: {
                x: {
                    velocity: deltaX * 40, // Higher number = movement amplified
                    end: 0 // Go back to the initial position
                },
                y: {
                    velocity: deltaY * 40, // Higher number = movement amplified
                    end: 0 // Go back to the initial position
                },
            },
        })
    })
})

Let’s add a second tween to bring a bit more life to our animation. This one will randomly rotate the image by an angle within a defined range.

Let’s take a closer look at the native Math.random() method, which returns a random value between 0 and 1. Following this logic, Math.random() - 0.5 returns a random value between -0.5 and 0.5. When multiplied by another value, it increases the range of variation. For example, to randomly rotate a media by an angle between -15 and 15 degrees, I use:

(Math.random() - 0.5) * 30

We’ll combine this calculation with the gsap.fromTo() method, which lets you define both the starting and ending values of an animation.

root.querySelectorAll('.medias div').forEach(el => {
    el.addEventListener('mouseenter', () => {
        ...

        tl.fromTo(media, {
            rotate: 0
        }, {
            duration: 0.4,
            rotate: (Math.random() - 0.5) * 30, // Returns a value between -15 & 15
            yoyo: true, 
            repeat: 1,
            ease: 'power1.inOut' // Will slow at the begin and the end
        }, '<') // Means that the animation starts at the same time as the previous tween
    })
})

You’ll notice we’re using the yoyo and repeat properties. The first makes the animation play in reverse once it completes, and the second sets how many times it will repeat. This way, the image rotates and then returns to its original angle.

Go further

To take this a step further, we could increase the z-index of each image when hovered, so it always appears on top of the others.

Effects like this can sometimes behave unpredictably on touch devices, especially on mobile. In such cases, I usually disable the effect and present a simpler layout to the user. If you’d like to see how to implement this, check out our Go Further page

Final code

<section class="mwg_effect000">
    <div class="header">
        <div>
            <p class="button button1">
                <img src="assets/medias/01.png" alt="">
                <span>3d & stuff</span>
            </p>
        </div>
        <div>12 items saved in your collection</div>
        <div>
            <p class="button button2">Add more</p>
        </div>
    </div>

    <div class="medias">
        <div class="media"><img src="assets/medias/01.png" alt=""></div>
        <div class="media"><img src="assets/medias/02.png" alt=""></div>
        <div class="media"><img src="assets/medias/03.png" alt=""></div>
        <div class="media"><img src="assets/medias/04.png" alt=""></div>
        <div class="media"><img src="assets/medias/05.png" alt=""></div>
        <div class="media"><img src="assets/medias/06.png" alt=""></div>
        <div class="media"><img src="assets/medias/07.png" alt=""></div>
        <div class="media"><img src="assets/medias/08.png" alt=""></div>
        <div class="media"><img src="assets/medias/09.png" alt=""></div>
        <div class="media"><img src="assets/medias/10.png" alt=""></div>
        <div class="media"><img src="assets/medias/11.png" alt=""></div>
        <div class="media"><img src="assets/medias/12.png" alt=""></div>
    </div>
</section>
.mwg_effect000 {
    height: 100vh;
    overflow: hidden;
    position: relative;
    display: grid;
    place-items: center;
}

.mwg_effect000 .header {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    border-bottom: 1px solid #323232;
    padding: 20px 25px;
    color: #BAB8B9;
}
.mwg_effect000 .header div:nth-child(2) {
    font-size: 26px;
}
.mwg_effect000 .header div:last-child {
    display: flex;
    justify-content: flex-end;
}
.mwg_effect000 .button {
    font-size: 14px;
    text-transform: uppercase;
    
    border-radius: 24px;
    height: 48px;
    gap: 5px;
    padding: 0 20px;
    display: flex;
    align-items: center;
    width: max-content; 
}
.mwg_effect000 .button1 {
    background-color: #232323;
}
.mwg_effect000 .button2 {
    border: 1px solid #323232;
}


.mwg_effect000 .button img {
    width: 22px;
    height: auto;
    display: block;
}

.mwg_effect000 .medias {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1vw;
}
.mwg_effect000 .medias img {
    width: 11vw;
    height: 11vw;
    object-fit: contain;
    border-radius: 4%;
    display: block;
    pointer-events: none;
    will-change: transform;
}

@media (max-width: 768px) {
    .mwg_effect000 .header {
        padding: 15px;
        display: flex;
        justify-content: space-between;
    }
    .mwg_effect000 .header div:nth-child(2) {
        display: none;
    }
    .mwg_effect000 .medias {
        gap: 2vw;
    }
    .mwg_effect000 .medias img {
        width: 18vw;
        height: 18vw;
    }
}
window.addEventListener("DOMContentLoaded", () => {
    gsap.registerPlugin(InertiaPlugin)

    let oldX = 0, 
        oldY = 0, 
        deltaX = 0,
        deltaY = 0
    
    const root = document.querySelector('.mwg_effect000')
    root.addEventListener("mousemove", (e) => {
        // Calculate horizontal movement since the last mouse position
        deltaX = e.clientX - oldX;

        // Calculate vertical movement since the last mouse position
        deltaY = e.clientY - oldY;

        // Update old coordinates with the current mouse position
        oldX = e.clientX;
        oldY = e.clientY;
    })

    root.querySelectorAll('.media').forEach(el => {

        // Add an event listener for when the mouse enters each media
        el.addEventListener('mouseenter', () => {
            
            const tl = gsap.timeline({ 
                onComplete: () => {
                    tl.kill()
                }
            })
            tl.timeScale(1.2) // Animation will play 20% faster than normal
            
            const image = el.querySelector('img')
            tl.to(image, {
               inertia: {
                    x: {
                        velocity: deltaX * 30, // Higher number = movement amplified
                        end: 0 // Go back to the initial position
                    },
                    y: {
                        velocity: deltaY * 30, // Higher number = movement amplified
                        end: 0 // Go back to the initial position
                    },
                },
            })
            tl.fromTo(image, {
                rotate: 0
            }, {
                duration: 0.4,
                rotate:(Math.random() - 0.5) * 30, // Returns a value between -15 & 15
                yoyo: true, 
                repeat: 1,
                ease: 'power1.inOut' // Will slow at the begin and the end
            }, '<') // The animation starts at the same time as the previous tween
        })
    })
})

3D

  • Womp

Photo

  • Ihza Akbar

Illustration

  • Sarah Fatmi

Wanna learn
something else?

Back to the collection