创建鼠标悬停时移动的 3D CSS 按钮

Button 通常是通往客户旅程的门户。因此,我玩弄了一个 3d 按钮的想法,该按钮可以移动用户在其周围移动鼠标。为了进一步提高这种效果,我添加了一些 3d 阴影,它们一起移动,以产生一个 3d 按钮的错觉,该按钮位于页面外,随着用户的鼠标移动而移动。

演示#

注意:尝试将鼠标悬停在这些按钮上。在移动设备上,点击将模仿您点击时的悬停状态。徘徊!?工作? 鼠标悬停⚙️ 另一个例子

演示也在 CodePen

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap" rel="stylesheet">
<div id="dark-light-container">
    <div class="white-container">
        <button class="button"><span>Hover!</span></button>
    </div>
    <div class="black-container">
        <button class="button green" data-filter-color="rgba(71, 255, 111, 0.4)"><span>? Work</span></button>
    </div>
    <div class="black-container">
        <button class="button animated" data-filter-color="#00a1ff57"><span>? Mouse Over</span></button>
    </div>
    <div class="white-container">
        <button class="button purple" data-filter-color="#e347ff8f" data-custom-perspective="900px"><span>⚙️ Another Example</span></button>
    </div>
</div>
<a href="https://fjolt.com/article/css-javascript-3d-depth-hover-buttons" target="_blank">Read Article Here</a>
    body {
      margin: 0;
      background: rgb(10 13 37);
      padding: 0;
    }
    #dark-light-container {
        float: left;
        width: 100%;
        margin: 0 0 0 0;
    }
a {
  color: rgba(255,255,255,0.5);
  font-size: 1rem;
  font-family: Inter, sans-serif;
  border-top: 1px solid rgba(255,255,255,0.2);
  display: block;
  font-size: 1.25rem;
  padding: 1rem 1.5rem;
  float: left;
  width: 100%;
}
    #dark-light-container > div {
        float: left;
        box-sizing: border-box;
        position: relative;
        padding: 2rem;
        width: 50%;
        text-align: center;

    }
    .white-container { background: white; }
    .black-container { background: black; }

    button {
        box-shadow: none;
        background: transparent;
      font-family: Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;
        transform-style: preserve-3d;
        padding: 0;
        height: auto;
        float: none;
        appearance: none;
        border: none;
        letter-spacing: 1px;
    }

    button span {
        background: linear-gradient(180deg, #ff7147, #e0417f);
        font-size: 2rem;
        color: white;
        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: 500;
        perspective-origin: 0 0;
        letter-spacing: 0;
    }

    button.animated span:after {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        width: 600%;
        height: 200%;
        background: linear-gradient(90deg, #00a1ff, #af4ae6, #00a1ff, #2cefff);
        animation: colorCycle 8s infinite alternate;
        z-index: -1;

    }
    button.green span {
        background: linear-gradient(180deg, #47ff6f, #417ce0);
        filter: drop-shadow(0 15px 15px rgba(71, 255, 111, 0.4));
    }

    button.animated span {
        filter: drop-shadow(0 15px 15px #00a1ff57);
    }

    button.purple span {
        background: linear-gradient(180deg, #e347ff, #7b41e0);
    }

    button.animated.example span:after {
        width: 200%;
        height: 100%;
        animation: colorCycleExample 4s infinite alternate;
    }
    
    @keyframes colorCycle {
        0% {
            top: 0;
            left: 0;
            transform: scale(1);
        }
        50% {
            top: -50%;
            transform: scale(1.4);
        }
        100% {
            top: 0;
            left: -500%;
            transform: scale(1);
        }
    }

    @keyframes colorCycleExample {
        0% {
            top: 0;
            left: 0;
            transform: scale(1);
        }
        100% {
            top: 0;
            left: -100%;
            transform: scale(1);
        }
    }

    @media screen and (max-width: 1000px) {
        #dark-light-container > div {
            width: 100%;
        }
    }

    let calculateAngle = function(e, item, parent) {
        let dropShadowColor = `rgba(0, 0, 0, 0.3)`
        if(parent.getAttribute('data-filter-color') !== null) {
            dropShadowColor = parent.getAttribute('data-filter-color');
        }

        // 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`

        if(parent.getAttribute('data-custom-perspective') !== null) {
            parent.style.perspective = `${parent.getAttribute('data-custom-perspective')}`
        }

        // 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})`;
    }

    document.querySelectorAll('.button').forEach(function(item) {
        item.addEventListener('mouseenter', function(e) {
            calculateAngle(e, this.querySelector('span'), this);
        });

        item.addEventListener('mousemove', function(e) {
            calculateAngle(e, this.querySelector('span'), this);
        });

        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})`;
        });
    })

它是如何工作的?#

这些按钮背后的基本概念是我们需要跟踪用户何时将鼠标悬停在按钮上、移动和鼠标移出。在鼠标悬停时,我们将移动按钮使其显示为 3d。在鼠标移出时,我们将重置它。

在我们开始使用 Javascript 之前,让我们让我们的按钮看起来不错。我们的 HTML 将如下所示:

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

我们的 CSS 看起来像这样:

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;
}

? 小背景动画

您可能已经注意到第三个按钮有一个背景动画。如果您对我是如何做到的感兴趣,我使用了一个通过动画移动的伪元素。伪元素有一个简单的渐变,溢出是隐藏的。如果我们去掉溢出,你可以更容易地看到它是如何工作的:? 鼠标悬停

Javascript 的工作原理#

现在让我们看看我们的Javascript。您可能已经注意到我们的按钮有两个元素 –按钮本身和其中的一个跨度。这有一个很好的理由——这让我们可以在父对象上应用 3d 透视图,这是效果工作所必需的。它还允许我们将悬停效果定位到父级 – 如果我们在子级上使用悬停,效果会在子级旋转时出错,并且我们会错过命中框。

我正在使用一个使用事件变量 ( e ) 的函数,并同时引用跨度(此处记为item)和按钮(引用为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})`;
}

实际上,这会将按钮分成 4 个象限。中点表示 X 轴和 Y 轴上 0 的变化角度,而向左移动会导致更负的 Y 角,向右移动更正的 Y 角。这同样适用于 X,向上移动光标会使 X 角更正,向下移动更负。

一些值得注意的事情:

  • 我们正在使用过滤器框阴影——这是因为它们使用 CSS 的transition属性可以更好地过渡。
  • 我添加了添加自定义透视和框阴影颜色的功能– 无需更改代码即可提供更大的灵活性。
  • 通过划分calcAngle*变量来调制效果。如果你改变你划分它们的多少,甚至改变视角,效果会或多或少地明显。

将我们的功能应用于每个按钮

要将我们所有的功能应用于每个按钮,我们只需使用forEach. 如果您想了解有关如何将 Javascript 事件添加到多个元素的更多信息,请单击以下链接。

    
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})`;
    });
})

我们完成了