In this tutorial we’ll have some fun with JavaScript mouse events by building something useful and interesting: thumbnails which, when hovered over, display a selection of different ebooks.
Here’s the demo that we’re going to build (move the mouse around over each thumbnail):
Each thumbnail displays the corresponding ebooks hidden underneath, depending on the cursor position. It’s an ideal way of giving a sneak preview of what’s available (in this case in the Envato Tuts+ ebook library).
As we build this we’ll be using CSS Grid Layout and the JavaScript mousemove
event, amongst other things.
1. The HTML Markup
The markup we’ll use is pretty straightforward; a plain unordered list with some equally-sized images inside each of the list items (cards).
<ul class="cards"> <li class="card"> <img src="IMG_SRC" alt=""> <!-- 4 more images here --> </li> <li class="card"> <img src="IMG_SRC" alt=""> <!-- 3 more images here --> </li> <li class="card"> <img src="IMG_SRC" alt=""> <!-- 2 more images here --> </li> <li class="card"> <img src="IMG_SRC" alt=""> <!-- 1 more image here --> </li> </ul>
2. The CSS
Now, with regards to the CSS styles, two things are important:
- We define the unordered list as a grid container and give the list items
width: 25%
. Instead of CSS grid, you could use flexbox or your preferred layout method. - We visually hide and absolutely position all images inside the list items, apart from the first one.
The CSS rules we apply to our demo are shown below:
.cards { display: grid; grid-gap: 20px; grid-template-columns: repeat(4, 1fr); } .card { position: relative; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.5); } .card:hover { cursor: pointer; } .card img:not(:first-of-type) { position: absolute; top: 0; right: 0; bottom: 0; left: 0; opacity: 0; } .card img.is-visible { opacity: 1; }
With a couple of other reset styles (removing the bullets from the unordered list, giving the body a background color etc.) we end up with this:
3. The JavaScript
Let’s add the following two lines so that when all page assets are ready, the init
function is executed. We’ll also trigger it every time the browser window is resized.
window.addEventListener("load", init); window.addEventListener("resize", init);
Inside this function, a number of things happen; first we loop through the cards.
function init() { const cards = document.querySelectorAll(".card"); cards.forEach(el => { // actions here }); }
Loop the Child Images
Next, for each card we retrieve the number of child images without taking into account the first image.
Note: first image is visible by default and not absolutely positioned.
const numOfChildImages = el.querySelectorAll("img:not(:first-of-type)").length;
If there’s at least one child image, we do the following things:
- Calculate the width of the card (the width of the first image) and...
- ...split the card into equal parts by dividing its width with the number of child images.
if (numOfChildImages > 0) { const { width } = el.getBoundingClientRect(); const parts = width / numOfChildImages; }
To better understand this, let’s assume that our first card is 235px wide. This card contains four images (remember we ignore the first image), so the division 235px/4 will give us 58.75px. So what is the role of this number? Well, it creates our ranges, so upon card hover, we track the mouse X position, check its range, and finally display the appropriate image.
For our simple example, here are the generated ranges:
Mouse X Position |
Image to Show |
---|---|
0<X≤58.75px |
1st |
58.75px<X≤117.5px |
2nd |
117.5px<X≤176.25px |
3rd |
176.25px<X≤235px | 4th |
Please note the “Image to Show” column shows the image that should appear from the pool of the four child images (once again we exclude the first visible image).

Now that we know the requirements, let’s translate them into code. Still inside the loop we listen for the mousemove
event.
// hover cards el.addEventListener("mousemove", e => { // do stuff here });
When this event fires, we perform the following actions:
- Get the X coordinate of the mouse pointer relative to the “hovered” card and not relative to the browser window.
- Remove the
is-visible
class from all card images. - Display the appropriate image depending on the mouse position (see previous table for an example).
Part of the code that implements the aforementioned behavior is as follows:
el.addEventListener("mousemove", e => { //1 const xPos = e.pageX - el.offsetLeft; //2 removeIsVisibleClass(); //3 switch (numOfChildImages) { case 1: if (xPos > 0 && xPos <= parts) { addClass(el, "img:nth-child(2)"); } break; case 2: if (xPos > 0 && xPos <= parts) { addClass(el, "img:nth-child(2)"); } else if (xPos > parts && xPos <= parts * 2) { addClass(el, "img:nth-child(3)"); } break; // more cases below } });
As you can see, there are two custom functions. Firstly, the removeIsVisibleClass
function which is responsible for removing the is-visible
class from the corresponding image. Secondly, the more general addClass
function which is responsible for adding the is-visible
class to the target image.
Here’s their signature:
function removeIsVisibleClass() { if (document.querySelector("img.is-visible")) { document.querySelector("img.is-visible").classList.remove("is-visible"); } } function addClass(parent, child, className = "is-visible") { parent.querySelector(child).classList.add(className); }
So far, we’ve seen what happens every time we hover over a card. Let’s now discuss the opposite scenario. In other words, what should happen if we stop hovering over a card. In this case the first initial image should be displayed:
// inside cards loop el.addEventListener("mouseleave", () => { removeIsVisibleClass(); });
4. Browser Support
Our demo should work well in most of the desktop browsers. A few notes though:
- The demo makes use of CSS Grid and the
foreach
loop which aren’t supported by all browsers. There are alternative solutions for both cases if you prefer to use a fallback. - The demo works similarly on all screens/devices and isn’t optimized for small screens/touch devices. For our simple demo that's fine, but in a real project you might want to restrict this implementation to larger screens only (or non-touch devices).
Finally, as usual, we use Babel to compile the ES6 code down to ES5.
Conclusion
In this tutorial, we managed to build an interesting hover effect by taking advantage of JavaScript mouse events. Hopefully you’ve been inspired enough to build something amazing.
To take things further, I encourage you to build a reusable function that will handle the repetitive code inside the switch
statement. Post a comment if you take the challenge on!
Envato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post