How to Create a Motion Hover Effect for a Background Image Grid

By Mary Lou


From our sponsor: Build skills to lead communication strategy, translate complex data, and drive user experience.

If you follow our UI Interactions & Animations Roundups, you might have spotted this beautiful grid designed by the folks of tubik:

Previously, Zhenya Rynzhuk also designed this wonderful layout with a similar interaction:

It’s not too complicated to implement this. I wanted to try it and in the following I’ll walk you through the relevant markup and code.

The markup and style for the grid

The markup is simply a grid of items that have background images. I like to use this structure because it allows me to control the sizes of the images by setting their position in the grid.

<div class="grid"> <div class="grid__item pos-1"> <div class="grid__item-img" style="background-image:url(img/1.jpg);"></div> </div> <div class="grid__item pos-2"> <div class="grid__item-img" style="background-image:url(img/2.jpg);"></div> </div> <div class="grid__item pos-3"> <div class="grid__item-img" style="background-image:url(img/3.jpg);"></div> </div> ...

The grid is stretched to be a bit bigger than its parent because we want to move the items and create the illusion of an infinite plane of images.

.grid { pointer-events: none; position: absolute; width: 110%; height: 110%; top: -5%; left: -5%; display: grid; grid-template-columns: repeat(50,2%); grid-template-rows: repeat(50,2%);
} .grid__item { position: relative;
} .grid__item-img { position: relative; width: 100%; height: 100%; background-size: cover; background-position: 50% 50%;

The grid is divided into 50 cells for the rows and columns. With this layout density, the position of each image element can be set precisely.

/* Shorthand grid-area: grid-row-start / grid-column-start / grid-row-end / grid-column-end */ .pos-1 { grid-area: 10 / 1 / 26 / 7;
} .pos-2 { grid-area: 1 / 18 / 9 / 27;
} .pos-3 { grid-area: 1 / 36 / 14 / 42;
} ...

Note that I use the double division structure for the possibility of moving the inner element with the background image to create the motion effect seen in demo 3. For that case, I define some extra styles:

/* If we want to move the inner image */ .grid--img .grid__item { overflow: hidden; display: flex; align-items: center; justify-content: center; will-change: transform;
} .grid--img .grid__item-img { flex: none; width: calc(100% + 100px); height: calc(100% + 100px); will-change: transform;

The JavaScript

Now, let’s have a look at the JavaScript part. I’m using GSAP by GreenSock. We start by creating a Grid class to represent the grid of pictures:

export default class Grid { constructor(el) { this.DOM = {el: el}; this.gridItems = []; this.items = [...this.DOM.el.querySelectorAll('.grid__item')]; this.items.forEach(item => this.gridItems.push(new GridItem(item))); this.showItems(); } ...
} const grid = new Grid(document.querySelector('.grid'));

There should be an initial animation where the grid items scale up and fade in. We can add a method to the class for that. We also want the items to start at different times and for that we use the GSAP stagger option. The items will start animating from the center of the grid:

showItems() { gsap.timeline() .set(this.items, {scale: 0.7, opacity: 0}, 0) .to(this.items, { duration: 2, ease: 'Expo.easeOut', scale: 1, stagger: {amount: 0.6, grid: 'auto', from: 'center'} }, 0) .to(this.items, { duration: 3, ease: 'Power1.easeOut', opacity: 0.4, stagger: {amount: 0.6, grid: 'auto', from: 'center'} }, 0);

Now, let’s make the items move as we move the mouse around. Each grid item will be represented by a GridItem class:

class GridItem { constructor(el) { this.DOM = {el: el}; this.move(); } ...

The position of each item in both axes should be mapped with the mouse position. So, the mouse can move from position 0 to the width or height of the window. As for the item, it’ll move in a range of [start, end] that we need to specify. We’ll be assigning random values for the start/end value so that each item moves differently from each other.

Let’s add the move method to the GridItem class:

move() { // amount to move in each axis let translationVals = {tx: 0, ty: 0}; // get random start and end movement boundaries const xstart = getRandomNumber(15,60); const ystart = getRandomNumber(15,60); // infinite loop const render = () => { // Calculate the amount to move. // Using linear interpolation to smooth things out. // Translation values will be in the range of [-start, start] for a cursor movement from 0 to the window's width/height translationVals.tx = lerp(translationVals.tx, map(mousepos.x, 0, winsize.width, -xstart, xstart), 0.07); translationVals.ty = lerp(translationVals.ty, map(mousepos.y, 0, winsize.height, -ystart, ystart), 0.07); gsap.set(this.DOM.el, {x: translationVals.tx, y: translationVals.ty}); requestAnimationFrame(render); } requestAnimationFrame(render);

And that’s it!

I hope you find this helpful and please let me know your feedback via @codrops. Thank you for reading!