Create moving 3D CSS buttons.

Recently I was playing with the idea of ​​a 3D button that allows the user to move their mouse around it. To further enhance this effect, I’ve added some 3D shadows that go hand in hand with the illusion of a 3D button that sits off the page and moves with the user’s mouse movement.


How it works?

The basic idea behind these buttons is that we need to track when the user moves the button, moves and the mouse comes out. On the mouseover, we will move the button so that it looks 3D. On mouseout, we will rearrange it.

Before we move on to JavaScript, let’s adjust our buttons. 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 it, I used a pseudo element that moves through the animation. The pseudo element has a simple inclination, and the overflow is hidden. You can remove it and test it yourself. overflow: hidden From span And button Elements.

How JavaScript works

Let’s take a look at our JavaScript. You may have noticed that we have two elements of our button: the button itself and a span inside it. There is a good reason for this. It also allows us to target parents for hover effect. If we use the hover on the child, the effect will be bug-out as the child rotates, and we will miss the hit box.

I am using a function that uses event variable (e) and refers to both spans (as noted here). item(And button) as referenced. 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.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 = `rotateY(${calcAngleX}deg) rotateX(${calcAngleY}deg) scale(1.15)`;
    // And set its container's perspective. = `${halfWidth * 2}px` = `${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. = `drop-shadow(${-calcShadowX}px ${calcShadowY}px 15px ${dropShadowColor})`;

It effectively divides the button into four quadrants. The middle point represents the angle of change on the X and Y axis of 0, while the movement on the left is more negative Y angle and the movement on the right is more positive. The same thing applies to X, where moving the cursor up makes the X angle more positive and the bottom more negative.

Here are some things to note:

  • We are using filter box shadows because they make better transitions with CSS transition Property
  • I’ve added the ability to add custom perspectives and box shadow colors to give more flexibility without changing the code.
  • The effect is modulated by splitting. calcAngle* Variables If you change how much you distribute them, or even change the perspective, the effect will be more or less obvious.

Applying our function to every button.

To apply our function to each button, we simply repeat through all of them. forEach.

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 are done

With that, we recreated the effect shown at the beginning of the article. We hope you enjoyed this guide.


Write a Comment

Your email address will not be published. Required fields are marked *