学习建立一个神奇的3D按钮

124 阅读11分钟

简介

我最近有一个很好的认识。按钮是网络的 "杀手锏"。

我们在网上做的每一件重要的事情,从订餐、预约到播放视频,都需要按一个按钮。按钮(以及它们提交的表格)使网络变得动态、互动和强大。

但是很多按钮都是乏善可陈的。它们可以在现实世界中引发巨大的变化,但它们完全没有实实在在的感觉。它们给人的感觉就像沉闷的日常像素。

预期受众

这是一个面向前端开发者的中级教程。它的重点是HTML/CSS,不需要JavaScript知识。

我们的策略

在本教程中,有一个主要的技巧,我们将使用几次,以创造一个3D按钮的幻觉

它是这样工作的:当用户与我们的按钮互动时,我们将在一个固定的背景前上下滑动一个前景层。

为什么不使用box-shadowborder ?这些属性对动画来说是非常昂贵的。如果我们想在按钮上有一个奶油般光滑的过渡,我们用这个策略会更成功。

下面是我们的MVP按钮的代码。

Code Playground

<style>
  .pushable {
    background: hsl(340deg 100% 32%);
    border-radius: 12px;
    border: none;
    padding: 0;
    cursor: pointer;
    outline-offset: 4px;
  }
  .front {
    display: block;
    padding: 12px 42px;
    border-radius: 12px;
    font-size: 1.25rem;
    background: hsl(345deg 100% 47%);
    color: white;
    transform: translateY(-6px);
  }

  .pushable:active .front {
    transform: translateY(-2px);
  }
</style>

<button class="pushable">
  <span class="front">
    Push me
  </span>
</button>

我们的button 元素提供了酒红色的背景颜色,模拟了我们的按钮的底边。我们还剥离了button 元素附带的默认边框/填充。

.front 是我们的前景层。它得到一个明亮的粉红色背景颜色,以及一些文本样式。

我们将用transform: translate 来滑动前景层。这是完成这种效果的最好方法,因为变换可以通过硬件加速。

当鼠标按住按钮的时候,:active 样式将被应用。我们将把前面的层往下移,使它位于底部以上2px。我们可以把它降到0px,但我想一直保持3D幻觉。

细节

我们已经创建了一个坚实的基础,现在是时候在它上面建立一些很酷的东西了

焦点轮廓

大多数浏览器会在按钮被点击时为其添加一个轮廓,以表明该元素已获得焦点。

这是默认情况下Chrome/MacOS上的样子。 image.png

在上面的MVP中,我擅自添加了一个outline-offset 声明。这个属性为我们的按钮提供了一些缓冲区。

.pushable {
  outline-offset: 4px;
}

image.png 这是一个巨大的改进,但它仍然有点碍眼。另外,它的工作并不稳定:在Firefox上,outline-offset 对默认的 "焦点 "轮廓不起作用。

但我们不能简单地删除它--那个轮廓对于使用键盘导航的人来说超级重要。他们依靠它来让他们知道哪个元素被关注。

幸运的是,我们可以使用一个豪华的CSS伪类来帮助我们。:focus-visible

Code Playground

<style>
  /* The CSS we saw earlier is still
   * being applied. It's tucked into
   * the "CSS" tab above.
   */

  .pushable:focus:not(:focus-visible) {
    outline: none;
  }
</style>

<button class="pushable">
  <span class="front">
    Push me
  </span>
</button>

这是一个非常重要的选择器,让我们把它分解一下。

:focus 伪类将在一个元素被聚焦时应用其声明。无论该元素是通过键盘上的tab键还是通过鼠标点击来关注它,这都是有效的。

:focus-visible 与此类似,但它只适用于当元素被聚焦*,并且*用户将受益于看到一个可视化的焦点指示器(例如,因为他们正在使用键盘导航)。

最后,:not ,允许我们混入一些逻辑。当元素与:focus 选择器相匹配时,这些样式将被应用,而不是 :focus-visible 选择器。在实际应用中,这意味着当按钮被聚焦并且用户使用指针设备(例如,鼠标、触控板、触摸屏上的手指)时,我们会隐藏轮廓。

浏览器支持和可访问性

我实话实说:上面的规则非常令人困惑!我们是否可以用更清晰的方式来解决这个问题?

有什么更清晰的方法可以达到同样的效果吗?如果我们改成这样写呢?

button {
  outline: none;
}
button:focus-visible {
  outline: revert;
}

revert 是一个特殊的关键字,根据浏览器的默认值*,它将恢复到应该有的值。在MacOS的Chrome浏览器中,这相当于一条蓝色实线。

这就更简单了,对吗?我们说的是 "隐藏轮廓,除非是在明显聚焦的时候"。

然而,不幸的是,这种替代方法有一个问题:它在旧的浏览器中不起作用

点击和焦点

在大多数浏览器中,点击一个按钮会使其聚焦。但是,根据你的浏览器和操作系统的不同,这可能对你来说并不正确

此外,在Safari中,按钮可以通过 "选项+Tab "来聚焦。在其他浏览器中,只用 "Tab "键就可以了。

悬停状态

所以,在现实生活中,按钮不会在你按下它之前升起来迎接你的手指。

但是,如果他们这样做,岂不是很酷?

让我们在按钮悬停时将其上移几个像素。另外,让我们在前面的图层上打上一个transition 。这将使状态变化成为动画,产生一个更流畅的互动。

编码游戏场

<style>
  .front {
    will-change: transform;
    transition: transform 250ms;
  }

  .pushable:hover .front {
    transform: translateY(-6px);
  }
  
  .pushable:active .front {
    transform: translateY(-2px);
  }
</style>

<button class="pushable">
  <span class="front">
    Push me
  </span>
</button>

我添加了will-change: transform 声明,这样这个动画就可以被硬件加速了。

注入个性

通过一个空白的transition: transform 250ms ,我们给了我们的按钮一个动画,但它仍然没有什么个性

让我们考虑一下可以在这个按钮上执行的不同动作。

  • 它可以被按下

  • 可以释放

  • 它可以被悬停

  • 可以离开(当用户用鼠标离开时)。

这些动作中的每一个都应该有相同的特征吗?我不这么认为。我想让按钮在被点击时迅速向下卡住,我想让它在释放时反弹回来。当光标游离时,我想让它以一种冰冷的速度沉回其自然位置。

下面是它的样子。试着与按钮进行交互,看看有什么不同。

代码操场

<style>
  .front {
    transition:
      transform
      600ms
      cubic-bezier(.3, .7, .4, 1);
  }

  .pushable:hover .front {
    transform: translateY(-6px);
    transition:
      transform
      250ms
      cubic-bezier(.3, .7, .4, 1.5);
  }

  .pushable:active .front {
    transform: translateY(-2px);
    transition: transform 34ms;
  }
</style>

<button class="pushable">
  <span class="front">
    Push me
  </span>
</button>

我们可以为每个状态设置重写,以改变动画的行为方式。除了选择不同的速度外,我们可以改变计时功能

我们的默认过渡,在.front ,当鼠标离开按钮时就会应用。它是我们的 "回归平衡 "过渡。我给了它一个600ms的悠闲的持续时间--当涉及到微观互动时,它是永恒的。我还通过cubic-bezier ,给它一个自定义的缓和曲线。

我很快会写更多关于立方贝塞尔曲线的文章。从本质上讲,它们让我们创建自己的时序曲线。这是一个较低级别的工具,给了我们大量的控制。

就我们的 "平衡 "曲线而言,它本质上是一个更积极的ease-out

当我们按下按钮时,我们切换到我们的:active 过渡。我选择了一个快如闪电的过渡时间,即34ms--在60fps下大约2帧。我希望这个过渡是快速的,因为人们在现实生活中往往就是这样按按钮的。

最后,我们的:hover 过渡。这个状态处理了两个独立的动作。

  • 鼠标移到按钮上时的升起

  • 松开按钮后的弹回。

理想情况下,我应该为每一个动作选择不同的过渡,但这在纯CSS中是不可能的。如果我真的想走得更远,我需要写一些JS来区分这些状态。

我设计了一个 "有弹性 "的贝塞尔曲线,它有点过冲。这使按钮更有个性。下面是这个曲线的样子。

最终,贝塞尔曲线看起来永远不会像弹簧物理学 那样郁郁葱葱,但它们可以通过足够的修饰而变得非常接近了

添加阴影

为了真正推销整个 "3D "的东西,我们可以添加一个阴影。

image.png

你可能很想用box-shadow 来完成这个任务,但我们重复之前看到的一个技巧,会有更大的成功。我们的阴影将是一个单独的图层,它将向我们的前层的相反方向移动。

为了使之发挥作用,我们需要对事物进行一些重组。这里是我们新设置的标记。

<button>
  <span class="shadow"></span>
  <span class="edge"></span>
  <span class="front">Push Me</span>
</button>

之前,我们使用<button> 本身作为我们的边缘层。但现在,我们需要在它下面有一个阴影。我们的<button> 将成为一个包装器,容纳3个层,一个层叠一个层。

下面是CSS,为了简洁起见,删去了一些内容。

代码乐园

<button class="pushable">
  <span class="shadow"></span>
  <span class="edge"></span>
  <span class="front">
    Push me
  </span>
</button>

为了堆叠HTML元素,我们使用绝对定位。最后一层,.front ,使用相对定位,因为我们需要1个内流子来给<button> 的宽度和高度。

我们可以纯粹依靠DOM的顺序;不需要z-index 来控制堆叠的顺序!

就如何设置translate :我们的阴影在与我们的前层相反的方向移动。阴影并没有完全从基线位置上移动那么远。虽然.front 上移了6px,但.shadow 只下移了4px。这是一个主观的选择;你可能喜欢不同的值。我们鼓励你进行实验。

我们还可以添加一点模糊,以获得一个更柔和、更自然的阴影。

这可以通过模糊滤镜来完成。

.shadow {
  filter: blur(4px);
}

颜色和美学

我们就快完成了,但我们还可以做两件小事来完成这个效果。

这第一件事是超级微妙的,但真的令人满意。我在 "边缘 "元素上应用了一个线性渐变,以使它看起来像圆角反射了较少的光线。

这是这个部分的CSS。

.edge {
  background: linear-gradient(
    to left,
    hsl(340deg 100% 16%) 0%,
    hsl(340deg 100% 32%) 8%,
    hsl(340deg 100% 32%) 92%,
    hsl(340deg 100% 16%) 100%
  );
}

我们就快成功了--让我们在这个圣代上撒上一颗樱桃,然后就可以了。 image.png

我们应该如何处理这个问题呢?我们可以调换颜色,但由于我们刚刚添加了梯度,这就有点复杂了。幸运的是,我们可以利用另一个CSS过滤器:brightness

.pushable {
  transition: filter 600ms;
}
.pushable:hover {
  transition: filter 250ms;
  filter: brightness(110%);
}

悬停时,按钮会变亮10%。这将影响所有3个层。filter 是一个令人惊讶的动画性能,所以我们不会给硬件带来太多压力。

移动增强功能

在移动设备上点击互动元素时,浏览器会在上面闪烁一个 "点击矩形"。 image.png

注意到快速闪动的灰色矩形吗?颜色在iOS和Android之间有所不同,但效果是不变的。

为什么它要这样做呢?嗯,这个方框可以作为有用的反馈,确认你已经成功地点击了目标。但我们的按钮提供了足够的反馈,所以我们在这种情况下不需要它。

我们可以通过这个声明来移除它。

.pushable {
  -webkit-tap-highlight-color: transparent;
}

还有一件事:在iOS上,如果按钮被按住一秒钟,手机会尝试选择按钮内的文本。 image.png

让我们把这个按钮变成不可选择的,以改善这种情况。

.pushable {
  user-select: none;
}

巨大的权力伴随着巨大的责任。在禁用旨在提高可用性的浏览器功能时,我们应该非常谨慎在这种情况下,我觉得很有信心,我们是在改善体验,而不是降低体验,但这些属性应该极少使用。

从按钮开始 现在我们在这里

这是一个相当长的旅程,但我希望你会同意,我们已经建立了一个非常令人满意的按钮。

它也很浮夸;你可能想对使用这种按钮的地方有所选择。我不会把这个按钮用在GDPR cookie同意的横幅上但是,如果你想做一些宏大而激动人心的动作,你现在有一个与🎉的按钮。

如果你对提高你的CSS技能感兴趣,我最近推出了一门课程!它是专门为JS开发人员定制的。它是专门为JS开发人员定制的。如果你使用React或Vue这样的框架,而你对CSS感觉不是很舒服,我今年的任务就是要改变这种状况。它被称为CSS for JavaScript Developers

✨ 这是我们的大型可推式按钮的最终源代码。

<style>
  .pushable {
    position: relative;
    border: none;
    background: transparent;
    padding: 0;
    cursor: pointer;
    outline-offset: 4px;
    transition: filter 250ms;
  }
  .shadow {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border-radius: 12px;
    background: hsl(0deg 0% 0% / 0.25);
    will-change: transform;
    transform: translateY(2px);
    transition:
      transform
      600ms
      cubic-bezier(.3, .7, .4, 1);
  }
  .edge {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border-radius: 12px;
    background: linear-gradient(
      to left,
      hsl(340deg 100% 16%) 0%,
      hsl(340deg 100% 32%) 8%,
      hsl(340deg 100% 32%) 92%,
      hsl(340deg 100% 16%) 100%
    );
  }
  .front {
    display: block;
    position: relative;
    padding: 12px 42px;
    border-radius: 12px;
    font-size: 1.25rem;
    color: white;
    background: hsl(345deg 100% 47%);
    will-change: transform;
    transform: translateY(-4px);
    transition:
      transform
      600ms
      cubic-bezier(.3, .7, .4, 1);
  }
  .pushable:hover {
    filter: brightness(110%);
  }
  .pushable:hover .front {
    transform: translateY(-6px);
    transition:
      transform
      250ms
      cubic-bezier(.3, .7, .4, 1.5);
  }
  .pushable:active .front {
    transform: translateY(-2px);
    transition: transform 34ms;
  }
  .pushable:hover .shadow {
    transform: translateY(4px);
    transition:
      transform
      250ms
      cubic-bezier(.3, .7, .4, 1.5);
  }
  .pushable:active .shadow {
    transform: translateY(1px);
    transition: transform 34ms;
  }
  .pushable:focus:not(:focus-visible) {
    outline: none;
  }
</style>
<button class="pushable">
  <span class="shadow"></span>
  <span class="edge"></span>
  <span class="front">
    Push me
  </span>
</button>