边框动画

Avatar of Stephen Shaw
Stephen Shaw

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

为悬停状态转换 边框。很简单,对吧?你可能会感到不愉快地惊讶。

挑战

挑战很简单:构建一个带有扩展边框的按钮。

本文将重点介绍真正的 CSS 技巧,这些技巧易于应用到任何项目中,而无需接触 DOM 或使用 JavaScript。此处介绍的方法将遵循以下规则

  • 单个元素(没有辅助 div,但允许使用伪元素)
  • 仅限 CSS(无 JavaScript)
  • 适用于任何尺寸(不受特定宽度、高度或纵横比的限制)
  • 支持透明背景
  • 平滑且性能良好的过渡

我在 Animation at Work SlackTwitter 上提出了这个挑战。虽然在最佳方法上没有达成共识,但我确实从一些杰出的开发人员那里得到了一些非常巧妙的想法。

方法 1:动画 border

动画边框最直接的方法是……好吧,通过动画 border

.border-button {
  border: solid 5px #FC5185;
  transition: border-width 0.6s linear;
}

.border-button:hover { border-width: 10px; }

查看 Shaw (@shshaw) 在 CodePen 上的 Pen

简洁明了,但存在一些重大的性能问题。

由于 border 在文档的布局中占据空间,因此更改 border-width触发布局。由于新的边框大小,附近的元素将四处移动,导致浏览器在动画的每一帧重新定位这些元素,除非您在按钮上设置了显式大小。

仿佛触发布局还不够糟糕,过渡本身感觉“分步”。我将在下一个示例中说明原因。

方法 2:使用 outline 改善 border

我们如何在不触发布局的情况下更改边框?通过使用 outline 代替!您可能最熟悉 outline 用于删除 :focus 样式中的 outline (尽管你不应该这样做),但 outline 是一条外部线条,不会更改元素在布局中的大小或位置。

.border-button {
  outline: solid 5px #FC5185;
  transition: outline 0.6s linear;
  margin: 0.5em; /* Increased margin since the outline expands outside the element */
}

.border-button:hover { outline-width: 10px; }

查看 Shaw (@shshaw) 在 CodePen 上的 Pen

在 Dev Tools 的“性能”选项卡中快速检查一下,显示 outline 过渡不会触发布局。无论如何,移动仍然显得分步,因为浏览器正在对 border-widthoutline-width 值进行舍入,因此您不会在 56 之间获得亚像素渲染,或者从 5.45.5 的平滑过渡。

查看 Shaw (@shshaw) 在 CodePen 上的 Pen

奇怪的是,Safari 通常不会渲染 outline 过渡,并且偶尔会留下疯狂的伪影。

border artifact in safari

方法 3:使用 clip-path 剪裁

首次实施Steve Gardner 完成,此方法使用 clip-pathcalc 来修剪边框,以便在悬停时可以过渡以显示完整的边框。

.border-button {  
  /* Full width border and a clip-path visually cutting it down to the starting size */
  border: solid 10px #FC5185; 
  clip-path: polygon( 
    calc(0% + 5px) calc(0% + 5px), /* top left */
    calc(100% - 5px) calc(0% + 5px), /* top right */
    calc(100% - 5px) calc(100% - 5px), /* bottom right */
    calc(0% + 5px) calc(100% - 5px) /* bottom left */
  );
  transition: clip-path 0.6s linear;
}

.border-button:hover {
  /* Clip-path spanning the entire box so it's no longer hiding the full-width border. */
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

查看 Shaw (@shshaw) 在 CodePen 上的 Pen

clip-path 技术到目前为止是最平滑、性能最高的方法,但确实有一些注意事项。舍入误差可能会导致一些不均匀,具体取决于确切的大小。边框也必须从一开始就为全尺寸,这可能会使精确定位变得棘手。

不幸的是,IE/Edge 尚未支持,尽管它似乎 正在开发中。您可以并且应该鼓励 Microsoft 的团队通过 投票添加蒙版/剪辑路径 来实现这些功能。

方法 4:linear-gradient 背景

我们可以使用多个正确大小的 linear-gradient 背景的巧妙组合来模拟边框。总共有四个单独的渐变,每个边一个。background-positionbackground-size 属性将每个渐变置于正确的位置和大小,然后可以对其进行过渡以使边框扩展。

.border-button {
  background-repeat: no-repeat;
  
  /* background-size values will repeat so we only need to declare them once */
  background-size: 
    calc(100% - 10px) 5px, /* top & bottom */
    5px calc(100% - 10px); /* right & left */
  
  background-position: 
    5px 5px, /* top */
    calc(100% - 5px) 5px, /* right */
    5px calc(100% - 5px), /* bottom */
    5px 5px; /* left */
  
  /* Since we're sizing and positioning with the above properties, we only need to set up a simple solid-color gradients for each side */
  background-image: 
    linear-gradient(0deg, #FC5185, #FC5185),
    linear-gradient(0deg, #FC5185, #FC5185),
    linear-gradient(0deg, #FC5185, #FC5185),
    linear-gradient(0deg, #FC5185, #FC5185);
  
  transition: all 0.6s linear;
  transition-property: background-size, background-position;
}

.border-button:hover {
  background-position: 0 0, 100% 0, 0 100%, 0 0;
  background-size: 100% 10px, 10px 100%, 100% 10px, 10px 100%;
}

查看 Shaw (@shshaw) 在 CodePen 上的 Pen

这种方法设置起来相当困难,并且跨浏览器存在一些差异。Firefox 和 Safari 以流畅的方式动画化伪边框,这正是我们想要的效果。Chrome 的动画很生硬,甚至比 outlineborder 过渡更分步。IE 和 Edge 完全拒绝动画化 background,但它们确实提供了正确的边框扩展效果。

方法 5:使用 box-shadow 伪造

box-shadow 规范中隐藏了第四个 spread-radius 值。将所有其他长度值设置为 0px,并使用 spread-radius 来构建您的 border 替代方案,该方案与 outline 一样,不会影响布局。

.border-button {
  box-shadow: 0px 0px 0px 5px #FC5185;
  transition: box-shadow 0.6s linear;
  margin: 0.5em; /* Increased margin since the box-shado expands outside the element, like outline */
}

.border-button:hover { box-shadow: 0px 0px 0px 10px #FC5185; }

查看 Shaw (@shshaw) 在 CodePen 上的 Pen

使用 box-shadow 的过渡性能足够好,感觉也流畅得多,除了在 Safari 中,它在过渡期间像 borderoutline 一样捕捉到整数值。

伪元素

其中几种技术可以修改为使用 伪元素,但伪元素最终在我的测试中导致了一些额外的性能问题。

对于 box-shadow 方法,过渡偶尔会在比必要更大的区域触发绘制。Reinier Kaper 指出 伪元素可以帮助将绘制隔离到更具体的区域。当我运行更多测试时,box-shadow 不再导致文档的大区域出现绘制,并且伪元素的复杂性最终导致性能下降。绘制和性能的变化可能是由于 Chrome 更新造成的,因此您可以自行测试。

我还没有找到一种能够以允许基于 transform 的动画的方式利用伪元素的方法。

为什么不使用 transform: scale

您可能正在打开 Twitter 以帮助建议为此使用 transform: scale。由于 transformopacity动画性能最佳的样式属性,为什么不使用伪元素并使边框放大和缩小呢?

.border-button {
  position: relative;
  margin: 0.5em;
  border: solid 5px transparent;
  background: #3E4377;
}

.border-button:after {
  content: '';
  display: block;
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  border: solid 10px #FC5185;
  margin: -15px;
  z-index: -1;
  transition: transform 0.6s linear;
  transform: scale(0.97, 0.93);
}

.border-button:hover::after { transform: scale(1,1); }

查看 Shaw (@shshaw) 在 CodePen 上的 Pen

有一些问题

  1. 边框将显示在透明按钮后面。我强制在按钮上设置背景以显示边框如何隐藏在按钮后面。如果您的设计要求按钮具有完整的背景,那么这可能有效。

  2. 你无法将边框缩放至特定尺寸。由于按钮的尺寸会随文本内容变化,因此仅使用 CSS 无法实现从精确 5px 到 10px 的边框动画效果。在这个例子中,我在 `scale` 属性上使用了几个魔法数字使其看起来正确,但这并不具有通用性。
  3. 边框动画效果不均匀是因为按钮的长宽比不是 1:1。这通常意味着在动画完成之前,左右两侧看起来会比上下两侧更大。这可能不是问题,具体取决于你的过渡速度、按钮的长宽比以及边框的大小。

如果你的按钮具有固定尺寸,Cher 指出了一种巧妙的方法来计算所需的精确缩放比例,尽管这可能会存在一些舍入误差。

超越 CSS

如果我们稍微放宽一下规则,就会发现很多有趣的方法来实现边框动画。Codrops 在这个领域一直做着出色的工作,通常利用 SVG 和 JavaScript。最终的效果非常令人满意,尽管实现起来可能有点复杂。以下是一些值得查看的示例

结论

边框不仅仅是简单的 `border`,但如果你想要为边框添加动画,可能会遇到一些麻烦。这里介绍的方法会提供帮助,尽管没有一个是完美的解决方案。你的选择将取决于项目的具体需求,因此我列了一个对比表来帮助你做出决定。

查看 Shaw 在 CodePen 上的 @shshaw 创作的 示例

我建议使用 `box-shadow`,它在易于实现、动画效果、性能和浏览器支持方面实现了最佳的整体平衡。

你还有其他创建动画边框的方法吗?也许可以使用巧妙的方法利用转换来移动边框?请在下方评论或通过 Twitter 联系我,分享你解决此挑战的方案。

特别感谢 Martin PittSteve GardnerCherReinier KaperJoseph RexDavid Khourshid 以及 Animation at Work 社区