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 theupdate()
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 ourmouse
object at the center of the element (e
).updatePosition()
: Updates the current position of ourmouse
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!
The CSS version :)
https://codepen.io/onediv/pen/BprVzp
I thought that was very clever, but… you’re using 100 empty anchors to produce the effect.
well done, but can not used in the production environment.
Would it be more performant to decouple the mouse events calculation from the style updates here?
Theoretically it would, but when I reduced the refreshRate to 1, tested, and compared, there really wasn’t any difference…..
Original with refreshRate down to 1: https://codepen.io/asiankingofwhales/pen/GxWOBL?editors=1010
Decoupling mouse calculation from style updates: https://codepen.io/asiankingofwhales/pen/VXprjX?editors=0010
Hi WeiLi Fan!
I think you will get a better understanding of how the
isTimeToUpdate
method if you comment these CSS lines:With an
updateRate
of 1 or 0, your inner div will be updated everytime your mouse moves (at each pixel)!The idea behind the
isTimeToUpdate
method is to lower the number of calls to theupdate
method. By doing so, we also lower the number of computations done by the client’s computer.However this produces a clunky transition between updates if left alone. Thats why we are applying CSS transitions! We now have a nice and smooth transition between each update.
Try setting your
updateRate
high enough and comment those CSS lines. You will see more clearly how often you actually compute the new 3D rotation for your inner div.Hope that helps! :)
Nice writeup. Did you https://micku7zu.github.io/vanilla-tilt.js/ though?
Hey Nico!
The main point behind this post is to provide an example of a cool CSS-Trick and explain how it can be done. :)
Nice finding by the way!