Animate a Container on Mouse Over Using Perspective and Transform

I’ve been working on a website in which large pictures are displayed to the user. Instead of creating a typical lightbox effect (a zoom-in animation with a black overlay) for these large pictures, I decided to try and make something more interactive and fun. I ended up coding an image container that tilts as the user moves the mouse cursor above it.

Here’s the final version:

See the Pen MrLopq by Mihai (@MihaiIonescu) on CodePen.

This effect is achieved through CSS and JavaScript. I figured I’d make a little tutorial explaining how each part works so you could easily reproduce it or extend it.

I recommend reading up on the almanac entries for perspective and transform before we get started. We’re going to refer to these properties through the post and it’s a good idea to be familiar with them.

Let’s get down to it.

Setup

First, we need a container with another inner element. The container will help with the perspective.

<div id="container">
  <div id="inner"></div>
</div>

For demonstration purposes, let’s center the card exactly in the middle of the screen:

body {
  /* Full screen width and height */
  width: 100%;
  min-height: 100vh;
    
  /* Centers the container in the middle of the screen */
  display: flex;
  justify-content: center;
  align-items: center;
    
  margin: 0;
  background-color: rgb(220, 220, 220);
}

#container {
  /* This will come into play later */
  perspective: 40px;
}

#inner {
  width: 20em;
  height: 18em;
  background-color: white;
}

This gives us a white card that is positioned directly in the center of a light gray background. Note that we’ve set the perspective of the #container to 40px which does nothing at this point because we have not created any transforms. That will be handled later in the JavaScript.

See the Pen 3D Image Container - Part 0 by Mihai (@MihaiIonescu) on CodePen.

Let’s Start Scripting

Here’s the outline for what we’re doing:

var container = document.getElementById('container');
var inner = document.getElementById('inner');
        
var onMouseEnterHandler = function(event) {
  update(event);
};
var onMouseLeaveHandler = function() {
  inner.style = "";
};
var onMouseMoveHandler = function(event) {
  if (isTimeToUpdate()) {
    update(event);
  }
};

container.onmouseenter = onMouseEnterHandler;
container.onmouseleave = onMouseLeaveHandler;
container.onmousemove = onMouseMoveHandler;

And here is what all those things are (or will) be doing:

  • Handler Functions: these functions handle the events as they happen. We want to decide what happens when the cursor enters, moves over, and leaves the container, so each of those has a handler.
  • Update Function: We haven’t coded this yet but its goal will be to update the 3D rotation of our #inner div.
  • Time to Update Function: This is another function we haven’t coded yet but it will return true when an update is required. This is a way to reduce the number of calls to the update() function and improve the performance of our script.
  • Event: This is a JavaScript object that describes the event that occurred.

The code above will:

  • Update the 3D rotation of the inner div as soon as the mouse enters the container.
  • Update the 3D rotation of the inner div when the appropriate time comes as the mouse moves over the container.
  • Reset the style of the inner div when the mouse leaves the container.

Is it Time to Update?

Let’s add the function that decides when to update the 3D rotation of the #inner div.

var counter = 0;
var updateRate = 10;
var isTimeToUpdate = function() {
  return counter++ % updateRate === 0;
};

When the counter reaches the updateRate, an update will be made.

At this point, you can try replacing the update function by a console.log() and play with the updateRate to see how it all works together.

The Mouse

Next up is the mouse object. This one is a little more complex than the other sections. Still, it’s not that difficult to understand, but the code can seem intimidating, especially if you’re new to JavaScript.

// Init
var container = document.getElementById('container');
var inner = document.getElementById('inner');
// Mouse 
var mouse = {
  _x: 0,
  _y: 0,
  x: 0,
  y: 0,
  updatePosition: function(event) {
    var e = event || window.event;
    this.x = e.clientX - this._x;
    this.y = (e.clientY - this._y) * -1;
  },
  setOrigin: function(e) {
    this._x = e.offsetLeft + Math.floor(e.offsetWidth/2);
    this._y = e.offsetTop + Math.floor(e.offsetHeight/2);
  },
  show: function() { return '(' + this.x + ', ' + this.y + ')'; }
}
// Track the mouse position relative to the center of the container.
mouse.setOrigin(container);

Again, let’s walk through this together.

  • show(): Displays the current position of the mouse (if you want to do some debugging in the browser’s console).
  • setOrigin(e): Sets the coordinates (0,0) of our mouse object at the center of the element (e).
  • updatePosition(): Updates the current position of our mouse object, relative to (0,0).

The last line of code mouse.setOrigin(container) snaps the coordinates (0,0) of our mouse object to the center of our container. Here's an example that illustrates it.

See the Pen 3D Image Container - Part 1 by Mihai (@MihaiIonescu) on CodePen.

The idea behind all this is to add more rotation to our #inner div as you move the mouse farther from the center of the container.

Update Styles on Mouse Position

Here’s our update function:

var update = function(event) {
  mouse.updatePosition(event);
  updateTransformStyle(
    (mouse.y / inner.offsetHeight/2).toFixed(2),
    (mouse.x / inner.offsetWidth/2).toFixed(2)
  );
};

var updateTransformStyle = function(x, y) {
  var style = "rotateX(" + x + "deg) rotateY(" + y + "deg)";
  inner.style.transform = style;
  inner.style.webkitTransform = style;
  inner.style.mozTransform = style;
  inner.style.msTransform = style;
  inner.style.oTransform = style;
};
  • update(): Updates the mouse position and updates the style of the #inner div.
  • updateTransformStyle(): Updates the style for each vendor prefix.

Are We Done?

We’d better do some testing! Looks like we get a change in perspective when the mouse cursor enters and exits the card, but it’s not as smooth as it could be:

See the Pen 3D Image Container - Part 2 by Mihai (@MihaiIonescu) on CodePen.

Oh right! We told it to update the rotation of our #inner div every time the counter hits the updateRate. This produces a clunky transition between updates.

How do we solve that? CSS transitions.

Adding Transitions

#inner {
  transition: transform 0.5s;
}

These are arbitrary numbers. You can play with the perspective and transform values to make the effect more or less dramatic as you see fit.

See the Pen 3D Image Container - Part 3 by Mihai (@MihaiIonescu) on CodePen.

Note that resizing the page will cause some problems because the position of the container changes in the page. The solution is to re-center your mouse object in your container after the page is resized.

Wrapping Up

We’re done! Now we have a container for making an element a little more interactive. The demo at the beginning of this post uses an image inside of the container, but this can be used for other things besides images, including forms, modals, or just about any other content you drop in the container. Go experiment!