为悬停状态转换 边框
。很简单,对吧?你可能会感到不愉快地惊讶。
挑战
挑战很简单:构建一个带有扩展边框的按钮。
本文将重点介绍真正的 CSS 技巧,这些技巧易于应用到任何项目中,而无需接触 DOM 或使用 JavaScript。此处介绍的方法将遵循以下规则
- 单个元素(没有辅助 div,但允许使用伪元素)
- 仅限 CSS(无 JavaScript)
- 适用于任何尺寸(不受特定宽度、高度或纵横比的限制)
- 支持透明背景
- 平滑且性能良好的过渡
我在 Animation at Work Slack 和 Twitter 上提出了这个挑战。虽然在最佳方法上没有达成共识,但我确实从一些杰出的开发人员那里得到了一些非常巧妙的想法。
border
方法 1:动画 动画边框最直接的方法是……好吧,通过动画 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
将 触发布局。由于新的边框大小,附近的元素将四处移动,导致浏览器在动画的每一帧重新定位这些元素,除非您在按钮上设置了显式大小。
仿佛触发布局还不够糟糕,过渡本身感觉“分步”。我将在下一个示例中说明原因。
outline
改善 border
方法 2:使用 我们如何在不触发布局的情况下更改边框?通过使用 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-width
和 outline-width
值进行舍入,因此您不会在 5
和 6
之间获得亚像素渲染,或者从 5.4
到 5.5
的平滑过渡。
查看 Shaw (@shshaw) 在 CodePen 上的 Pen。
奇怪的是,Safari 通常不会渲染 outline
过渡,并且偶尔会留下疯狂的伪影。

clip-path
剪裁
方法 3:使用 首次实施 由 Steve Gardner 完成,此方法使用 clip-path
和 calc
来修剪边框,以便在悬停时可以过渡以显示完整的边框。
.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 的团队通过 投票添加蒙版/剪辑路径 来实现这些功能。
linear-gradient
背景
方法 4:我们可以使用多个正确大小的 linear-gradient
背景的巧妙组合来模拟边框。总共有四个单独的渐变,每个边一个。background-position
和 background-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 的动画很生硬,甚至比 outline
和 border
过渡更分步。IE 和 Edge 完全拒绝动画化 background
,但它们确实提供了正确的边框扩展效果。
box-shadow
伪造
方法 5:使用 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 中,它在过渡期间像 border
和 outline
一样捕捉到整数值。
伪元素
其中几种技术可以修改为使用 伪元素,但伪元素最终在我的测试中导致了一些额外的性能问题。
对于 box-shadow
方法,过渡偶尔会在比必要更大的区域触发绘制。Reinier Kaper 指出 伪元素可以帮助将绘制隔离到更具体的区域。当我运行更多测试时,box-shadow
不再导致文档的大区域出现绘制,并且伪元素的复杂性最终导致性能下降。绘制和性能的变化可能是由于 Chrome 更新造成的,因此您可以自行测试。
我还没有找到一种能够以允许基于 transform
的动画的方式利用伪元素的方法。
为什么不使用 transform: scale
?
您可能正在打开 Twitter 以帮助建议为此使用 transform: scale
。由于 transform
和 opacity
是 动画性能最佳的样式属性,为什么不使用伪元素并使边框放大和缩小呢?
.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。
有一些问题
- 边框将显示在透明按钮后面。我强制在按钮上设置背景以显示边框如何隐藏在按钮后面。如果您的设计要求按钮具有完整的背景,那么这可能有效。
- 你无法将边框缩放至特定尺寸。由于按钮的尺寸会随文本内容变化,因此仅使用 CSS 无法实现从精确 5px 到 10px 的边框动画效果。在这个例子中,我在 `scale` 属性上使用了几个魔法数字使其看起来正确,但这并不具有通用性。
- 边框动画效果不均匀是因为按钮的长宽比不是 1:1。这通常意味着在动画完成之前,左右两侧看起来会比上下两侧更大。这可能不是问题,具体取决于你的过渡速度、按钮的长宽比以及边框的大小。
如果你的按钮具有固定尺寸,Cher 指出了一种巧妙的方法来计算所需的精确缩放比例,尽管这可能会存在一些舍入误差。
超越 CSS
如果我们稍微放宽一下规则,就会发现很多有趣的方法来实现边框动画。Codrops 在这个领域一直做着出色的工作,通常利用 SVG 和 JavaScript。最终的效果非常令人满意,尽管实现起来可能有点复杂。以下是一些值得查看的示例
结论
边框不仅仅是简单的 `border`,但如果你想要为边框添加动画,可能会遇到一些麻烦。这里介绍的方法会提供帮助,尽管没有一个是完美的解决方案。你的选择将取决于项目的具体需求,因此我列了一个对比表来帮助你做出决定。
查看 Shaw 在 CodePen 上的 @shshaw 创作的 示例。
我建议使用 `box-shadow`,它在易于实现、动画效果、性能和浏览器支持方面实现了最佳的整体平衡。
你还有其他创建动画边框的方法吗?也许可以使用巧妙的方法利用转换来移动边框?请在下方评论或通过 Twitter 联系我,分享你解决此挑战的方案。
特别感谢 Martin Pitt、Steve Gardner、Cher、Reinier Kaper、Joseph Rex、David Khourshid 以及 Animation at Work 社区。
在使用 `box-shadow` 时,我可以在悬停状态取消后看到该框。
非常棒的方法对比!谢谢。
Umar,你使用的是哪个浏览器/平台?在我的测试中,我没有遇到 Safari 中的轮廓问题那样的伪影问题。
我也是。看起来像是 Chrome 的问题,我在 Firefox 中尝试过,没有注意到这个问题。
在 Firefox Quantum 中也存在同样的问题 :/
我偶尔会在 Chrome 中看到 `box-shadow` 出现这种情况,但在 Firefox 或 Safari 中没有遇到过。
我找到的一个解决方案是使用 `outline: solid 10px transparent`,其中 10px 是扩展后的边框总大小。这似乎可以消除 Chrome 的渲染伪影
我看到的唯一问题是,某些移动设备上的动画有点卡顿。不过,这发生在旧版本的 Chrome 中。
非常喜欢这篇文章!感谢您一如既往地提供精彩的内容。 :)
谢谢,Joe!
这些技巧在不同的浏览器中确实存在一些卡顿或阶梯感。“动画”列中的概述表主要对这一点进行了评级。
非常令人满意的文章。我喜欢每个概念是如何简要解释的。
这个网站总是提供精心制作的内容,这就是我唯一订阅通知的网站的原因
Fawad Tariq
UI/UX 设计师和前端开发人员
我同意你的观点,解释得非常好!
我可能忽略了这个解决方案,但为什么不同时为边框和内边距添加动画呢。
好想法,Jermaine!
由于 `border` 值会被四舍五入,你仍然会遇到我提到的阶梯动画效果。此外,当移除内边距以扩展边框时,边框会向元素的中心收缩,而不是从元素向外扩展。
这两点都不是什么大问题,但并非我想要的效果。
这种方法最大的问题是性能。`border` 更改会触发布局,并且将其与内边距更改结合起来意味着渲染和绘制的成本更高,尽管页面布局在视觉上并没有发生变化。
你的解决方案是唯一一个将边框向按钮内部扩展的方案,这很有趣。
关于内边距,边框的增加为 1rem(左右两侧分别从 0.5rem 增加到 1rem),为了在左右两侧留出这个空间,你只需要为 1rem 留出空间,分布在两侧,所以只需减少 0.5rem 的内边距。
希望对你有帮助
我是不是错过了什么?为什么不直接使用 (1) 以及 `box-sizing: border-box;` 呢?
由于按钮没有固定尺寸(例如 `width: 200px`),因此边框尺寸的更改仍会更改布局。
在没有查看你的解决方案的情况下,仅根据初始需求,我首先使用了边框的严格动画(以及一个用于补偿周围空间的边距),然后是 `box-shadow`。
这个 CodePen (https://codepen.io/mcbenny/pen/pdBVxd) 展示了这两种效果。
使用 `box-shadow` 方法的一个有趣之处在于 `border-radius` 也会应用于框的“内部”,在我看来这更漂亮(如果圆角仍然是一种趋势 ;-))。
但是 `border` 技术允许使用“双”样式边框…
很棒的演示,Ben!`box-shadow` 也是我的首选方案。
关于 `box-shadow` 保留 `border-radius` 的说明很好。正如你指出的那样,`border` 确实提供了更多细粒度的控制(各个边、替代样式等),但目前的性能和动画质量严重不足。希望浏览器将继续优化 `border`,以便在未来提供更好的控制。
伪元素怎么样?(不过按钮不能是透明的)
查看 Janusz Kaliszczak 在 CodePen 上的 JOQQvK 示例 (@kali187)。
https://production-assets.codepen.io/assets/embed/ei.js
好主意,Janusz!由于需要背景,我省略了类似的方法。这里的性能也会很差,因为你需要为位置添加动画,并且不幸的是 `top`、`left` 等也会被四舍五入为整数,所以你会遇到我在前两种方法中提到的阶梯效果。
也许像这样?
查看 Janusz Kaliszczak 在 CodePen 上的 gXVroP 示例 (@kali187)。
Firefox 57 对方法 2 的渲染非常流畅。这并没有解决问题,但我发现它很有趣。