Creating 3D CSS Buttons which Move as you Mouse Over

📣 Sponsor

Button are often the gateway to customer journeys. As such I was toying with the idea of a 3d button which moves a the user moves their mouse around it. To further this effect, I added some 3d shadows which move in tandem to give the illusion of a 3d button which is sitting off the page, which moves with the user's mouse movements.


Note: try hovering over these buttons. On mobile, tapping will mimic the hover state at the point you tap.

Demo also on CodePen

How does it work?

The fundamental concept behind these buttons are that we need to track when the user mouses over the button, moves, and mouses out. On mouse over, we will move the button so it appears 3d. On mouse out, we will reset it.

Before we get to the Javascript, let's make our button look good. Our HTML will look like this:

<button class="button"><span>Hover!</span></button>

And our CSS looks like this:

button { box-shadow: none; background: transparent; transform-style: preserve-3d; padding: 0; height: auto; float: none; } button span { background: linear-gradient(180deg, #ff7147, #e0417f); font-size: 2rem; padding: 1rem 2rem; line-height: 3rem; will-change: transform, filter; float: none; margin: 0; transition: all 0.15s ease-out; height: auto; border-radius: 100px; overflow: hidden; display: block; margin: 0px auto; display: block; transform: rotateX(0deg) rotateY(0deg) scale(1); filter: drop-shadow(0 15px 15px rgba(0,0,0,0.3)); font-weight: 600; perspective-origin: 0 0; letter-spacing: 0; }

🎨 Minor background animations

You may have noticed that the third button has a background animation. If you're interested in how I did that, I used a pseudo element which is moving via an animation. The pseudo element has a simple gradient, and the overflow is hidden. If we take the overflow off, you can see more easily how this works:

How the Javascript works

Let's take a look at our Javascript now. You may have noted we have two elements for our button - the button itself, and a span inside of it. There is a good reason for this - this lets us apply 3d perspective on the parent, which is required for the effect to work. It also allows us to target the parent for the hover effect - if we use hover on the child, the effect will bug out as the child will rotate, and we will miss the hitbox.

I am using a function which uses the event variable (e), and references both the span (noted here as item), and the button (referenced as parent).

let calculateAngle = function(e, item, parent) { let dropShadowColor = `rgba(0, 0, 0, 0.3)` // If the button has a data-filter-color attribute, then use this for the shadow's color if(parent.getAttribute('data-filter-color') !== null) { dropShadowColor = parent.getAttribute('data-filter-color'); } // If the button has a data-custom-perspective attribute, then use this as the perspective. if(parent.getAttribute('data-custom-perspective') !== null) { parent.style.perspective = `${parent.getAttribute('data-custom-perspective')}` } // Get the x position of the users mouse, relative to the button itself let x = Math.abs(item.getBoundingClientRect().x - e.clientX); // Get the y position relative to the button let y = Math.abs(item.getBoundingClientRect().y - e.clientY); // Calculate half the width and height let halfWidth = item.getBoundingClientRect().width / 2; let halfHeight = item.getBoundingClientRect().height / 2; // Use this to create an angle. I have divided by 6 and 4 respectively so the effect looks good. // Changing these numbers will change the depth of the effect. let calcAngleX = (x - halfWidth) / 6; let calcAngleY = (y - halfHeight) / 4; // Set the items transform CSS property item.style.transform = `rotateY(${calcAngleX}deg) rotateX(${calcAngleY}deg) scale(1.15)`; // And set its container's perspective. parent.style.perspective = `${halfWidth * 2}px` item.style.perspective = `${halfWidth * 3}px` // Reapply this to the shadow, with different dividers let calcShadowX = (x - halfWidth) / 3; let calcShadowY = (y - halfHeight) / 3; // Add a filter shadow - this is more performant to animate than a regular box shadow. item.style.filter = `drop-shadow(${-calcShadowX}px ${calcShadowY}px 15px ${dropShadowColor})`; }

Effectively this splits the button into 4 quadrants. The mid point represents an angle of change on the X and Y axis of 0, while a movement to the left results in a more negative Y angle, and a more positive one to the right. The same applies for X, where moving the cursor up turns the X angle more positive, and down, more negative.

Some things worth noting:

  • We are using filter box-shadows - and that's because they transition better with CSS' transition property.
  • I've added the ability to add custom perspective and box-shadow colors - to give more flexibility without having to change the code.
  • The effect is modulated by dividing the calcAngle* variables. If you change how much you divide them by, or even change the perspective, the effect will become more or less pronounced.

Applying our function to each button

To apply all our function to each button, we simply iterate through them all with forEach. Click the following link if you want to learn more about how to add Javascript events to multiple elements.

document.querySelectorAll('.button').forEach(function(item) { // Add on mouseenter item.addEventListener('mouseenter', function(e) { calculateAngle(e, this.querySelector('span'), this); }); // Add on mousemove item.addEventListener('mousemove', function(e) { calculateAngle(e, this.querySelector('span'), this); }); // Reset everything on mouse leave item.addEventListener('mouseleave', function(e) { let dropShadowColor = `rgba(0, 0, 0, 0.3)` if(item.getAttribute('data-filter-color') !== null) { dropShadowColor = item.getAttribute('data-filter-color') } item.querySelector('span').style.transform = `rotateY(0deg) rotateX(0deg) scale(1)`; item.querySelector('span').style.filter = `drop-shadow(0 10px 15px ${dropShadowColor})`; }); })

We're done

After that, we will have recreated the effect at the start of the article. We hope you've enjoyed this guide - here are some useful links:

Last Updated 1632607401334

More Tips and Tricks for CSS

Subscribe for Weekly Dev Tips

Subscribe to our weekly newsletter, to stay up to date with our latest web development and software engineering posts via email. You can opt out at any time.

Not a valid email