使用 CSS 创建定向发光 3D 按钮

2025-06-09

使用 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')
Enter fullscreen mode Exit fullscreen mode

这里我们创建了一个 mixin 来渲染 Twitter 图标的 SVG。如果我们像这样调用它,它就会渲染 Twitter 图标。

+icon()
Enter fullscreen mode Exit fullscreen mode

这样做会给我们一个大的 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)
Enter fullscreen mode Exit fullscreen mode

然后我们的 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')
Enter fullscreen mode Exit fullscreen mode

但是,我们可以传递一个键。然后,如果我们想要使用或重复使用多个图标,就可以将路径存储在一个对象中。

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')
Enter fullscreen mode Exit fullscreen mode

这是一种创建可复用的图标混合宏的简洁方法。对于我们的例子来说,这有点夸大其词,但值得注意。

现在,我们需要为按钮添加一些标记。

.scene
  button.button
    span.button__shadow
      +icon('Twitter')
    span.button__content
      +icon('Twitter')
      span.button__shine
Enter fullscreen mode Exit fullscreen mode

注意可访问性总是好的。我们可以通过查看开发者工具中的“辅助功能”面板来检查按钮的显示效果。

辅助功能面板显示按钮读取的文本

或许span在按钮文字中添加一个 ,并用 隐藏图标是个好主意aria-hidden。我们也可以隐藏span文本,同时让屏幕阅读器可以读取。

.scene
  button.button
    span.button__shadow
      +icon('Twitter')
    span.button__content
      span.button__text Twitter
      +icon('Twitter')
      span.button__shine
Enter fullscreen mode Exit fullscreen mode

我们有不同的方法来应用这些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])
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

再次检查“辅助功能”面板,按钮上只显示“Twitter”。这正是我们想要的!

风格

这是你们来这里的目的。我们如何设计它。首先,我们把这个放进去了;

* {
  transform-style: preserve-3d;
}
Enter fullscreen mode Exit fullscreen mode

这样我们就可以创建按钮所需的 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;
}
Enter fullscreen mode Exit fullscreen mode

在开始制作按钮之前,我们需要先倾斜场景。我们可以使用 来实现transform。在这里,我们链接 ,transform使其处于我们想要的位置。我花了一些时间在直播中调整这里的数值,使其更接近原始值。

.scene {
  height: var(--size);
  position: relative;
  width: var(--size);
  transform: rotateX(-40deg) rotateY(18deg) rotateX(90deg);
}
Enter fullscreen mode Exit fullscreen mode

你还会注意到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;
}
Enter fullscreen mode Exit fullscreen mode

这些是我们正在处理的变量,当我们构建按钮时这些变量将是有意义的。

按钮

让我们转到按钮!

首先,是实际的按钮元素。它将填充场景元素。我们可以直接在按钮上应用大小调整和变换。但是,如果我们要添加其他按钮和元素,就必须对它们全部进行变换和大小调整。这在使用 CSS 时通常需要注意。尝试让容器元素决定布局。

.button {
  appearance: none;
  background: none;
  border: 0;
  cursor: pointer;
  height: 100%;
  outline: transparent;
  position: absolute;
  width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

这里我们去掉了按钮样式。这样就得到了这个。

接下来,我们需要为按钮内容和阴影创建一个共同的起点。我们可以通过为每个元素赋予绝对定位来实现。内容将根据我们之前定义的深度变量进行 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));
}
Enter fullscreen mode Exit fullscreen mode

请注意我们也是如何利用--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);
}
Enter fullscreen mode Exit fullscreen mode

快完成了!不过图标目前大小不一样。我们会解决这个问题的。

让我们把按钮按下到位。这部分集成起来非常快。

.button__content {
  transition: transform var(--transition);
}
.button:active {
  --depth: 0;
}
Enter fullscreen mode Exit fullscreen mode

就是这样!使用作用域 CSS 变量,我们说的是删除 上的 z 轴平移。在 上:active添加可以使其不再那么即时。transitiontransform

剩下要做的就是设置按钮图层和光泽的样式。我们先从阴影开始。

.button__shadow {
  background: var(--bg-shadow);
  filter: blur(var(--blur));
  transition: filter var(--transition);
}
.button:active {
  --blur: 0;
}
Enter fullscreen mode Exit fullscreen mode

这里又添加了一个作用域样式。我们设定的是,当按下按钮时,阴影不再模糊。为了模糊阴影,我们使用了filterblur滤镜的 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);
}
Enter fullscreen mode Exit fullscreen mode

我们使用 的值--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);
}
Enter fullscreen mode Exit fullscreen mode

这个absolute定位将决定图标的大小。应用边框半径会使聚光灯变成圆形。我们filter再次使用它来添加模糊的聚光灯效果。你会注意到我们brightness在最后链接了一个滤镜,以便在模糊之后使物体稍微变亮。

使用 3D 平移可以确保光泽位于按钮上方,避免与其他元素发生深度冲突而导致光泽被遮挡。

目前样式设置就这些了。接下来该写脚本了。

脚本

为了方便起见,我们今天将使用GreenSock。它有一些简洁的实用程序可以满足我们的需求。但是,我们可以使用原生 JavaScript 实现同样的效果。因为我们使用的脚本类型为“module”,所以我们可以利用SkyPack

import gsap from 'https://cdn.skypack.dev/gsap'
Enter fullscreen mode Exit fullscreen mode

现在我们就可以开始修改了。我们希望按钮能够响应指针的移动。首先,我们需要让光晕平移,就像跟随指针一样。其次,我们需要根据指针的位置移动按钮。

让我们抓住我们需要的元素并在文档上设置一些基本的事件监听器。

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)
Enter fullscreen mode Exit fullscreen mode

尝试在此演示中移动您的指针来查看我们退回的贵重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%));
}
Enter fullscreen mode Exit fullscreen mode

然后,在更新函数中,我们计算发光的新位置。我们基于按钮大小的百分比来计算。我们可以用指针位置减去按钮位置来计算。然后,我们将所得结果除以位置。最后,乘以 200 得到百分比。

const BOUNDS = CONTENT.getBoundingClientRect()
const POS_X = ((x - BOUNDS.x) / BOUNDS.width) * 200
const POS_Y = ((y - BOUNDS.y) / BOUNDS.height) * 200
Enter fullscreen mode Exit fullscreen mode

例如,POS_X

  1. 抓取指针位置 x。
  2. 减去按钮位置 x。
  3. 除以按钮宽度。
  4. 乘以 200。

我们乘以 200,因为光泽是按钮大小的一半。这个部分比较棘手,因为我们要跟踪指针并将其映射到 3D 空间中。

要将其应用于按钮,我们可以使用 设置这些 CSS 变量gsap.set。这是一种 GSAP 方法,其工作方式类似于零秒补间动画。它在设置元素值时特别有用。

gsap.set(SHINE, {
  '--x': POS_X,
  '--y': POS_Y
})
Enter fullscreen mode Exit fullscreen mode

但是,如果我们想更进一步,我们可以使用quickSetterGSAP,这对于在进行大量更新的实际场景中的表现会更好。

const xySet = gsap.quickSetter(SHINE, 'css')
// Then to update the values
xySet({
  '--x': POS_X,
  '--y': POS_Y
})
Enter fullscreen mode Exit fullscreen mode

这使得我们的更新函数看起来像这样。

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
  })
}
Enter fullscreen mode Exit fullscreen mode

跟踪指针的准确性需要更多计算才能达到精确。请尝试一下这个演示,其中按钮上的溢出可见,并且光泽更加突出。您可以看到光泽元素如何失去跟踪。

此演示将所有内容放在了其应在的位置。

最后一个功能。让我们移动按钮来增加触感。在这里,我们将根据指针位置来移动按钮。但是,我们会限制它的移动。为此,我们可以使用另一个 GSAP 实用程序。我们将使用 mapRange。它允许我们将一组值映射到另一组值。然后,我们可以传入一个值并返回映射后的值。

首先,我们需要定义移动的限制。该限制是按钮尺寸的百分比。

const LIMIT = 10
Enter fullscreen mode Exit fullscreen mode

现在,我们可以在更新函数中计算偏移的百分比。我们通过将窗口宽度映射到边界来实现这一点。然后输入指针位置即可获取映射后的百分比。

const xPercent = gsap.utils.mapRange(
  0,
  window.innerWidth,
  -LIMIT,
  LIMIT,
  x
)
Enter fullscreen mode Exit fullscreen mode

0在这个代码块中,我们将的范围映射window.innerWidth。传递指针位置将返回一个介于 和 之间的值-10然后,我们可以将该百分比偏移应用到按钮上。我们对垂直偏移执行相同的操作,这将返回一个如下所示的更新函数。10x-1010

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,
  })
}
Enter fullscreen mode Exit fullscreen mode

就是这样!

这就是用 CSS 和一些脚本创建一个定向发光 3D 按钮的方法。更酷的是,我们可以相对轻松地进行修改。

在最终的演示中,我添加了一些额外的细节并更改了图标。你可能会认出来。

一如既往,感谢您的阅读。想了解更多?快来Twitter上关注我,或者观看直播

鏂囩珷鏉ユ簮锛�https://dev.to/jh3y/creating-directively-lit-3d-buttons-with-css-2jc2
PREV
使用无服务器绘制你的 Github 个人资料
NEXT
CSS动画Google字体