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.
Demo
Note: try hovering over these buttons. On mobile, tapping will mimic the hover state at the point you tap.
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:
- Source code on CodePen
- How to add Javascript events to multiple elements
- Learn how CSS Transforms Work
More Tips and Tricks for CSS
- How to Make your text look crisp in CSS
- Making 3D CSS Flippable Cards
- CSS Pixel Art Generator
- How the CSS Box Model Works
- Centering Elements in CSS with Tailwind
- CSS Pseudo Elements
- CSS Inset Borders at Varying Depths
- Stripe-Like Smooth Transition CSS Only Menu
- CSS Fonts
- The Interactive Guide to CSS Grid