使用 CSS 创建定向发光 3D 按钮
我不太清楚自己是怎么偶然发现这个帖子的。不过,有什么东西引导我看到了这条推文。
对我来说,这是一个挑战。我把它带到了直播中!
按钮设计很巧妙。但我不想直接照搬。我们决定做一个“Twitter”按钮。我们的想法是,创建一个几乎透明的按钮,上面放一个社交图标。然后,这个社交图标会在下方投射阴影。鼠标移动到按钮上,图标就会发光。按下按钮,图标就会被推到表面上。
今天,我们来看看如何做到这一点。很酷的是,你可以把图标换成任何你想要的。
标记
我创建这类内容的首要方法是搭建标记框架。初次检查后,我们需要复制使用的社交图标。一个巧妙的方法是使用 Pug 并利用 mixin。
mixin icon()
svg.button__icon(role='img' xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')
title Twitter icon
path(d='M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z')
这里我们创建了一个 mixin 来渲染 Twitter 图标的 SVG。如果我们像这样调用它,它就会渲染 Twitter 图标。
+icon()
这样做会给我们一个大的 Twitter 图标。
因为社交图标集倾向于使用相同的“0 0 24 24” viewBox
,所以我们可以制作标题和路径参数。
mixin icon(title, path)
svg.button__icon(role='img' xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')
title= title
path(d=path)
然后我们的 Twitter 图标变成
+icon('Twitter Icon', 'M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z')
但是,我们可以传递一个键。然后,如果我们想要使用或重复使用多个图标,就可以将路径存储在一个对象中。
mixin icon(key)
-
const PATH_MAP = {
Twitter: "M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"
}
svg.button__icon(role='img' xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')
title= `${key} Icon`
path(d=PATH_MAP[key])
+icon('Twitter')
这是一种创建可复用的图标混合宏的简洁方法。对于我们的例子来说,这有点夸大其词,但值得注意。
现在,我们需要为按钮添加一些标记。
.scene
button.button
span.button__shadow
+icon('Twitter')
span.button__content
+icon('Twitter')
span.button__shine
注意可访问性总是好的。我们可以通过查看开发者工具中的“辅助功能”面板来检查按钮的显示效果。
或许span
在按钮文字中添加一个 ,并用 隐藏图标是个好主意aria-hidden
。我们也可以隐藏span
文本,同时让屏幕阅读器可以读取。
.scene
button.button
span.button__shadow
+icon('Twitter')
span.button__content
span.button__text Twitter
+icon('Twitter')
span.button__shine
我们有不同的方法来应用这些aria-hidden
属性。我们将使用的方法是通过修改 mixin 代码来应用aria-hidden
。
mixin icon(key)
-
const PATH_MAP = {
Twitter: "...path code"
}
svg.button__icon(role='img' aria-hidden="true" xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')
title= `${key} Icon`
path(d=PATH_MAP[key])
Pug 的另一个巧妙方法是将所有属性传递给 mixin。这在只想传递部分属性的场景下非常有用。
mixin icon(key)
-
const PATH_MAP = {
Twitter: "...path code"
}
svg.button__icon(role='img' xmlns='http://www.w3.org/2000/svg' viewbox='0 0 24 24')&attributes(attributes)
title= `${key} Icon`
path(d=PATH_MAP[key])
再次检查“辅助功能”面板,按钮上只显示“Twitter”。这正是我们想要的!
风格
这是你们来这里的目的。我们如何设计它。首先,我们把这个放进去了;
* {
transform-style: preserve-3d;
}
这样我们就可以创建按钮所需的 3D 变换了。在最终的演示中尝试关闭此功能,你会发现一切都崩溃了。
让我们将 span 文本隐藏起来,让它不被屏幕阅读器看到。有很多方法可以做到这一点。推荐一种隐藏元素的方法,让我们可以不被屏幕阅读器看到,但可以避免屏幕阅读器看到,那就是使用这些样式。
.button__text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
在开始制作按钮之前,我们需要先倾斜场景。我们可以使用 来实现transform
。在这里,我们链接 ,transform
使其处于我们想要的位置。我花了一些时间在直播中调整这里的数值,使其更接近原始值。
.scene {
height: var(--size);
position: relative;
width: var(--size);
transform: rotateX(-40deg) rotateY(18deg) rotateX(90deg);
}
你还会注意到size
那里有一个变量。我们将使用 CSS 变量来驱动按钮的某些功能。这样可以方便地修改值和效果。通常,我们会将它们放在需要它们的范围内。但是,对于像这样的演示,将它们放在:root
文件顶部更方便我们操作。
:root {
--blur: 8px;
--shine-blur: calc(var(--blur) * 4);
--size: 25vmin;
--transition: 0.1s;
--depth: 3vmin;
--icon-size: 75%;
--radius: 24%;
--shine: rgba(255,255,255,0.85);
--button-bg: rgba(0,0,0,0.025);
--shadow-bg: rgba(0,0,0,0.115);
--shadow-icon: rgba(0,0,0,0.35);
--bg: #e8f4fd;
}
这些是我们正在处理的变量,当我们构建按钮时这些变量将是有意义的。
按钮
让我们转到按钮!
首先,是实际的按钮元素。它将填充场景元素。我们可以直接在按钮上应用大小调整和变换。但是,如果我们要添加其他按钮和元素,就必须对它们全部进行变换和大小调整。这在使用 CSS 时通常需要注意。尝试让容器元素决定布局。
.button {
appearance: none;
background: none;
border: 0;
cursor: pointer;
height: 100%;
outline: transparent;
position: absolute;
width: 100%;
}
这里我们去掉了按钮样式。这样就得到了这个。
接下来,我们需要为按钮内容和阴影创建一个共同的起点。我们可以通过为每个元素赋予绝对定位来实现。内容将根据我们之前定义的深度变量进行 3D 平移。
.button__content,
.button__shadow {
border-radius: var(--radius);
display: grid;
height: 100%;
place-items: center;
position: absolute;
width: 100%;
}
.button__content {
transform: translate3d(0, 0, var(--depth));
}
请注意我们也是如何利用--radius
变量的。
在这个阶段,很难区分这两个图标。现在是时候为它们添加样式了。我们可以应用一些基本的图标样式,并为每个 SVG 图标使用范围填充。
.button__content {
--fill: var(--icon-fill);
}
.button__shadow {
--fill: var(--shadow-fill);
}
.button__icon {
height: var(--icon-size);
fill: var(--fill);
width: var(--icon-size);
}
快完成了!不过图标目前大小不一样。我们会解决这个问题的。
让我们把按钮按下到位。这部分集成起来非常快。
.button__content {
transition: transform var(--transition);
}
.button:active {
--depth: 0;
}
就是这样!使用作用域 CSS 变量,我们说的是删除 上的 z 轴平移。在 上:active
添加可以使其不再那么即时。transition
transform
剩下要做的就是设置按钮图层和光泽的样式。我们先从阴影开始。
.button__shadow {
background: var(--bg-shadow);
filter: blur(var(--blur));
transition: filter var(--transition);
}
.button:active {
--blur: 0;
}
这里又添加了一个作用域样式。我们设定的是,当按下按钮时,阴影不再模糊。为了模糊阴影,我们使用了filter
带blur
滤镜的 CSS 属性。该属性的值已在 CSS 变量中定义。试试这个--blur
变量,看看会发生什么。
对于内容层,我们将使用背景色,然后应用背景滤镜。与滤镜类似,背景滤镜backdrop-filter
是一种为元素应用视觉效果的方法。目前常见的用例是使用模糊效果来呈现“玻璃态”。
.button__content {
backdrop-filter: blur(calc(var(--blur) * 0.25));
background: var(--button-bg);
overflow: hidden;
transition: transform var(--transition), backdrop-filter var(--transition);
}
我们使用 的值--blur
,并为 施加一个过渡效果backdrop-filter
。由于我们将--blur
变量的作用域限定在 上:active
,因此几乎可以不费吹灰之力就实现了过渡效果。为什么是overflow: hidden
?因为我们预计 shine 元素会在按钮周围移动。但我们不希望它游离到按钮之外。
现在,拼图的最后一块。那道光芒。这就是导致图标大小不一致的原因。因为它没有样式,所以影响了布局。让我们给它添加一些样式。
.button__shine {
--shine-size: calc(var(--size) * 0.5);
background: var(--shine);
border-radius: 50%;
height: var(--shine-size);
filter: blur(var(--shine-blur)) brightness(1.25);
position: absolute;
transform: translate3d(0, 0, 1vmin);
width: var(--shine-size);
}
这个absolute
定位将决定图标的大小。应用边框半径会使聚光灯变成圆形。我们filter
再次使用它来添加模糊的聚光灯效果。你会注意到我们brightness
在最后链接了一个滤镜,以便在模糊之后使物体稍微变亮。
使用 3D 平移可以确保光泽位于按钮上方,避免与其他元素发生深度冲突而导致光泽被遮挡。
目前样式设置就这些了。接下来该写脚本了。
脚本
为了方便起见,我们今天将使用GreenSock。它有一些简洁的实用程序可以满足我们的需求。但是,我们可以使用原生 JavaScript 实现同样的效果。因为我们使用的脚本类型为“module”,所以我们可以利用SkyPack。
import gsap from 'https://cdn.skypack.dev/gsap'
现在我们就可以开始修改了。我们希望按钮能够响应指针的移动。首先,我们需要让光晕平移,就像跟随指针一样。其次,我们需要根据指针的位置移动按钮。
让我们抓住我们需要的元素并在文档上设置一些基本的事件监听器。
import gsap from 'https://cdn.skypack.dev/gsap'
const BUTTON = document.querySelector('.button')
const CONTENT = document.querySelector('.button__content')
const SHINE = document.querySelector('.button__shine')
const UPDATE = ({x, y}) => console.info({x, y})
document.addEventListener('pointermove', UPDATE)
document.addEventListener('pointerdown', UPDATE)
尝试在此演示中移动您的指针来查看我们退回的贵重x
物品y
。
这是最棘手的部分。我们需要一些数学知识来计算闪耀的位置。我们将在初始重置后平移闪耀。我们需要先更新闪耀样式以适应这一点。我们使用了作用域 CSS 变量--x
和--y
。我们给它们设置了一个回退值 ,-150
这样当演示加载时,它们就不会出现在镜头中。
.button__shine {
top: 0;
left: 0;
transform: translate3d(-50%, -50%, 1vmin) translate(calc(var(--x, -150) * 1%), calc(var(--y, -150) * 1%));
}
然后,在更新函数中,我们计算发光的新位置。我们基于按钮大小的百分比来计算。我们可以用指针位置减去按钮位置来计算。然后,我们将所得结果除以位置。最后,乘以 200 得到百分比。
const BOUNDS = CONTENT.getBoundingClientRect()
const POS_X = ((x - BOUNDS.x) / BOUNDS.width) * 200
const POS_Y = ((y - BOUNDS.y) / BOUNDS.height) * 200
例如,POS_X
。
- 抓取指针位置 x。
- 减去按钮位置 x。
- 除以按钮宽度。
- 乘以 200。
我们乘以 200,因为光泽是按钮大小的一半。这个部分比较棘手,因为我们要跟踪指针并将其映射到 3D 空间中。
要将其应用于按钮,我们可以使用 设置这些 CSS 变量gsap.set
。这是一种 GSAP 方法,其工作方式类似于零秒补间动画。它在设置元素值时特别有用。
gsap.set(SHINE, {
'--x': POS_X,
'--y': POS_Y
})
但是,如果我们想更进一步,我们可以使用quickSetter
GSAP,这对于在进行大量更新的实际场景中的表现会更好。
const xySet = gsap.quickSetter(SHINE, 'css')
// Then to update the values
xySet({
'--x': POS_X,
'--y': POS_Y
})
这使得我们的更新函数看起来像这样。
const UPDATE = ({x, y}) => {
const BOUNDS = CONTENT.getBoundingClientRect()
const POS_X = ((x - BOUNDS.x) / BOUNDS.width) * 200
const POS_Y = ((y - BOUNDS.y) / BOUNDS.height) * 200
xySet({
'--x': POS_X,
'--y': POS_Y
})
}
跟踪指针的准确性需要更多计算才能达到精确。请尝试一下这个演示,其中按钮上的溢出可见,并且光泽更加突出。您可以看到光泽元素如何失去跟踪。
此演示将所有内容放在了其应在的位置。
最后一个功能。让我们移动按钮来增加触感。在这里,我们将根据指针位置来移动按钮。但是,我们会限制它的移动。为此,我们可以使用另一个 GSAP 实用程序。我们将使用 mapRange。它允许我们将一组值映射到另一组值。然后,我们可以传入一个值并返回映射后的值。
首先,我们需要定义移动的限制。该限制是按钮尺寸的百分比。
const LIMIT = 10
现在,我们可以在更新函数中计算偏移的百分比。我们通过将窗口宽度映射到边界来实现这一点。然后输入指针位置即可获取映射后的百分比。
const xPercent = gsap.utils.mapRange(
0,
window.innerWidth,
-LIMIT,
LIMIT,
x
)
0
在这个代码块中,我们将的范围映射window.innerWidth
到。传递指针位置将返回一个介于 和 之间的值-10
。然后,我们可以将该百分比偏移应用到按钮上。我们对垂直偏移执行相同的操作,这将返回一个如下所示的更新函数。10
x
-10
10
const buttonSet = gsap.quickSetter(BUTTON, 'css')
const xySet = gsap.quickSetter(SHINE, 'css')
const LIMIT = 10
const UPDATE = ({x, y}) => {
const BOUNDS = CONTENT.getBoundingClientRect()
const POS_X = ((x - BOUNDS.x) / BOUNDS.width) * 200
const POS_Y = ((y - BOUNDS.y) / BOUNDS.height) * 200
xySet({
'--x': POS_X,
'--y': POS_Y
})
const xPercent = gsap.utils.mapRange(
0,
window.innerWidth,
-LIMIT,
LIMIT,
x
)
const yPercent = gsap.utils.mapRange(
0,
window.innerHeight,
-LIMIT,
LIMIT,
y
)
buttonSet({
xPercent,
yPercent,
})
}
就是这样!
这就是用 CSS 和一些脚本创建一个定向发光 3D 按钮的方法。更酷的是,我们可以相对轻松地进行修改。
在最终的演示中,我添加了一些额外的细节并更改了图标。你可能会认出来。
一如既往,感谢您的阅读。想了解更多?快来Twitter上关注我,或者观看直播!