简介
我最近有一个很好的认识。按钮是网络的 "杀手锏"。
我们在网上做的每一件重要的事情,从订餐、预约到播放视频,都需要按一个按钮。按钮(以及它们提交的表格)使网络变得动态、互动和强大。
但是很多按钮都是乏善可陈的。它们可以在现实世界中引发巨大的变化,但它们完全没有实实在在的感觉。它们给人的感觉就像沉闷的日常像素。
预期受众
这是一个面向前端开发者的中级教程。它的重点是HTML/CSS,不需要JavaScript知识。
我们的策略
在本教程中,有一个主要的技巧,我们将使用几次,以创造一个3D按钮的幻觉。
它是这样工作的:当用户与我们的按钮互动时,我们将在一个固定的背景前上下滑动一个前景层。
为什么不使用box-shadow
或border
?这些属性对动画来说是非常昂贵的。如果我们想在按钮上有一个奶油般光滑的过渡,我们用这个策略会更成功。
下面是我们的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上的样子。
在上面的MVP中,我擅自添加了一个outline-offset
声明。这个属性为我们的按钮提供了一些缓冲区。
.pushable {
outline-offset: 4px;
}
这是一个巨大的改进,但它仍然有点碍眼。另外,它的工作并不稳定:在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 "的东西,我们可以添加一个阴影。
你可能很想用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%
);
}
我们就快成功了--让我们在这个圣代上撒上一颗樱桃,然后就可以了。
我们应该如何处理这个问题呢?我们可以调换颜色,但由于我们刚刚添加了梯度,这就有点复杂了。幸运的是,我们可以利用另一个CSS过滤器:brightness
。
.pushable {
transition: filter 600ms;
}
.pushable:hover {
transition: filter 250ms;
filter: brightness(110%);
}
悬停时,按钮会变亮10%。这将影响所有3个层。filter
是一个令人惊讶的动画性能,所以我们不会给硬件带来太多压力。
移动增强功能
在移动设备上点击互动元素时,浏览器会在上面闪烁一个 "点击矩形"。
注意到快速闪动的灰色矩形吗?颜色在iOS和Android之间有所不同,但效果是不变的。
为什么它要这样做呢?嗯,这个方框可以作为有用的反馈,确认你已经成功地点击了目标。但我们的按钮提供了足够的反馈,所以我们在这种情况下不需要它。
我们可以通过这个声明来移除它。
.pushable {
-webkit-tap-highlight-color: transparent;
}
还有一件事:在iOS上,如果按钮被按住一秒钟,手机会尝试选择按钮内的文本。
让我们把这个按钮变成不可选择的,以改善这种情况。
.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>