I am asking for a review of this code to see if I am following common best practices or if there is a better way to accomplish the goal set for this code.

My purpose was to create an image that would CSS3 rotateY() to your mouse's position over it with as close to a semantic approach as I could get and scalable to any size.

My approach was a simple Div->Img tag structure, the div's size is indirectly the image's size due to the design limitation of listeners in JS being attached to the div (this is so the rotation's effect on size doesn't jitter the image around).

Anyway, if I could get some critique I would be very appreciative. Thank you for reading!

Live Version

HTML index.html

<!DOCTYPE html>
<html lang="en">
    <title>Hover Card</title>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="css/style.css">
    <div class="container">
      <div class="hover-card">
        <img src="images/me.jpg" alt="A picture of Torben Leif.">

    <script src="scripts/main.js"></script>


.container {
  margin: calc(50vh - 125px) auto;
  width: 250px;
  height: 250px;

.hover-card {
  width: 230px;
  height: 230px;

.hover-card img {
  width: 100%;
  height: 100%;

  border-radius: 20px;
  overflow: hidden;
  object-fit: cover;
  object-position: center 20%; /* Specific To Avatar Image */

  transition: .2s linear;
  transform-style: preserve-3d;
  transform: perspective(600px) rotateY(var(--js-hover-rotate-val));


var card = document.querySelector('.hover-card');
var img = document.querySelector('.hover-card img');
var hoverComplete = true; // Used For Smooth Transitions While Rotating.
var degrees = 0; // External For Freeze Fix.

img.addEventListener('transitionend', (e) => {
  if(e.propertyName == 'transform')
    hoverComplete = true;

card.addEventListener('mousemove', (e) => {
  if(hoverComplete) {
    let newDegrees = Math.floor((1 - (e.pageX - card.getBoundingClientRect().left) / card.offsetWidth) * 90 - 45) * -1;
    if(newDegrees !== degrees) { // Freeze Fix
      degrees = newDegrees;
      img.style.setProperty('--js-hover-rotate-val', degrees + 'deg');
      hoverComplete = false;

card.addEventListener('mouseleave', (e) => {
  img.style.setProperty('--js-hover-rotate-val', '0deg')
  hoverComplete = false;

I find the calculation of tilt a bit complicated. If you know maximum tilt in degrees that shall happen on the left and right edges, you could declare it and a calculation function as

const MAX_INCLINE = 28; // <- just a guess

const shift = (w, x, limit = MAX_INCLINE) => (x / w * 2 - 1) * limit;

Next, Element#getBoundingClientRect, in my opinion, is unnecessary, because you receive all the necessary information in the instance of Event passed to your event listener function:

  • event.target.width is the width of target element that listens to mouse event, meaning the image in your case, and
  • event.offsetX is the position of mouse on X-axis relative to the element, meaning it's closer to 0 when the cursor is closer to the left edge, and closer to X when the cursor is closer to the right edge, where X is width of the element (image).

Considering this, the event listener function would look as simple as

(event) => {
  const { width } = event.target;
  const { offsetX } = event;

  image.style.setProperty('--js-hover-rotate-val', `${shift(width, offsetX)}deg`);

A codepen with the final code might be also useful.

  • 1
    \$\begingroup\$ Thank you for your reply and analysis, I greatly appreciate the feedback. Your shift function is undoubtedly more elegant than mine, I was also unaware offsetX worked in this manner. I'll take your advice into consideration in the future. \$\endgroup\$
    – user159882
    Feb 6 '18 at 20:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy