☰
纯CSS炫彩流光按钮:conic-gradient与@property实战
2026/10/17 5:18:47 网站建设 项目流程

1. 从"炫彩流光按钮"说起:一个被低估的前端练手项目

第一次看到"炫彩流光按钮"这个需求,是在一个后台管理系统的改版讨论里。产品经理指着设计稿说,登录页那个按钮太素了,能不能加个"光在边框上跑"的效果。当时团队里几个前端的第一反应都是"找个库吧",但翻了一圈发现,现成的组件库要么体积太大,要么动效僵硬得像PPT动画。最后还是决定手写一个,结果从下午两点折腾到晚上八点,中间踩的坑比想象中多得多。

这个效果看起来简单——不就是让一圈光绕着按钮转吗?但真正动手才会发现,里面涉及的知识点相当密集:CSS的conic-gradient怎么用、@property如何让角度动起来、伪元素怎么配合mask做遮罩、filter和backdrop-filter的性能差异、以及最关键的——怎么让光"跑"得自然而不是像卡顿的进度条。它几乎把CSS动画、渐变、遮罩、合成层这几个核心模块串了一遍,是个绝佳的练手项目。

这篇文章适合两类人:一是刚学完CSS基础、想找个有视觉冲击力的项目练手的初学者;二是做过一些动效、但对conic-gradient和@property配合使用还不太熟的进阶开发者。我会从最朴素的实现方案讲起,逐步迭代到最终效果,把每一步"为什么这么做"讲清楚,包括我踩过的那些坑——比如为什么你的流光会闪烁、为什么加了filter之后动画掉帧、为什么在移动端看起来完全不一样。

先明确一下最终要达成的效果:一个圆角矩形按钮,边框上有一道彩色光带持续环绕流动,光带经过的地方有发光感,按钮内部保持深色或半透明背景,鼠标悬停时流光速度加快、发光增强。整个效果纯CSS实现,不依赖任何JavaScript,兼容主流现代浏览器。

2. 拆解流光效果:三个必须搞懂的核心机制

在动手写代码之前,有必要把"流光"这个视觉效果拆解成可实现的CSS机制。很多人一上来就复制网上的代码,改改颜色就用了,结果遇到问题完全不知道怎么调。我习惯先把效果分解成独立的层次,每一层用最合适的CSS特性去实现,这样出问题的时候能快速定位是哪一层的事。

2.1 光带本体:conic-gradient才是正解

最早我尝试用linear-gradient做光带,思路是让一个线性渐变沿着边框移动。但很快就发现行不通——linear-gradient的方向是固定的,你只能通过background-position去平移它,而按钮是圆角的,平移过程中光带在圆角处的表现完全失控,要么被裁掉,要么变形。

正确的做法是用conic-gradient(圆锥渐变)。它以一个中心点为轴,沿着角度方向做渐变,天然适合做"环绕"效果。一个典型的流光渐变长这样:

background: conic-gradient( from var(--angle), transparent 0deg, #ff6b6b 60deg, #ffd93d 120deg, #6bcb77 180deg, #4d96ff 240deg, transparent 300deg, transparent 360deg );

这里from var(--angle)是关键,它决定了整个渐变从哪个角度开始。只要让--angle从0deg动到360deg,光带就会绕着中心转一圈。渐变里用transparent留出空白区域,这样同一时刻只有一段弧线是亮的,形成"光带"而不是"整圈都亮"。

注意:conic-gradient的兼容性在2020年之后的主流浏览器里已经没问题了,但如果你的项目需要支持更老的版本,得准备降级方案,比如用linear-gradient做静态渐变兜底。

2.2 让角度动起来:@property是灵魂

有了conic-gradient,接下来的问题是:怎么让--angle这个自定义属性动起来?如果你直接写transition: --angle 3s linear,会发现完全没效果。原因是CSS自定义属性默认是"不可插值"的——浏览器把它当成一个普通字符串,不知道怎么从0deg过渡到360deg。

这就是@property出场的地方。它允许你声明一个自定义属性的类型,一旦类型明确(比如<angle>),浏览器就知道怎么做插值动画了:

@property --angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }

这三行是流光效果能"动"的前提。syntax指定类型,initial-value给初始值,inherits一般设为false避免污染子元素。声明之后,你就可以在动画里直接操作--angle了:

@keyframes spin { to { --angle: 360deg; } }

实测下来,@property在Chrome、Edge、Safari 16.4+、Firefox 128+上都支持了。如果你的目标用户还在用更老的Safari,那这个方案会直接失效——这是这个效果最大的兼容性门槛,后面我会讲降级思路。

2.3 发光与遮罩:伪元素的分工

光带本身只是一条彩色的弧线,要让它看起来"发光",还需要额外的处理。我的做法是用两个伪元素分工:

  • ::before负责画光带本体,用conic-gradient加@property动画;
  • ::after负责做发光扩散,用同样的渐变但加filter: blur(),叠在光带下面。

然后按钮本身用一个深色背景盖在中间,只露出边框那一圈光。这里有个细节:光带要"只在边框上显示",所以需要用mask或者内层元素遮挡的方式,把中间区域挖空。常见做法是给按钮设置padding,内层放一个背景色和按钮背景一致的span,把中间盖住。

.btn { position: relative; background: #1a1a2e; border-radius: 12px; overflow: hidden; } .btn::before { content: ''; position: absolute; inset: -2px; background: conic-gradient(from var(--angle), transparent, #ff6b6b, #ffd93d, transparent); animation: spin 3s linear infinite; z-index: -1; }

用inset: -2px让伪元素比按钮大一圈,这样光带会从按钮边缘"溢出"一点点,形成边框发光的感觉。z-index: -1把它压到按钮背景下面,只露出边缘部分。

3. 从零手写:一份可以直接抄的完整实现

理论讲完了,接下来是实操。我会给出一份完整的、经过实测的代码,然后逐段解释每个参数为什么这么设。你可以直接把这段代码复制到一个HTML文件里打开看效果,不需要任何构建工具。

3.1 完整代码与逐行注释

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>炫彩流光按钮</title> <style> /* 第一步:声明可动画的自定义属性 */ @property --angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; } body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #0f0f1a; font-family: system-ui, -apple-system, sans-serif; } .glow-btn { position: relative; padding: 16px 48px; font-size: 18px; font-weight: 600; color: #fff; background: #1a1a2e; border: none; border-radius: 14px; cursor: pointer; isolation: isolate; overflow: hidden; transition: transform 0.2s ease; } /* 光带本体 */ .glow-btn::before { content: ''; position: absolute; inset: -3px; border-radius: inherit; background: conic-gradient( from var(--angle), transparent 0deg, #ff6b6b 40deg, #ffd93d 90deg, #6bcb77 140deg, #4d96ff 190deg, #b86bff 240deg, transparent 290deg, transparent 360deg ); animation: rotate 3s linear infinite; z-index: -2; } /* 发光扩散层 */ .glow-btn::after { content: ''; position: absolute; inset: -3px; border-radius: inherit; background: conic-gradient( from var(--angle), transparent 0deg, #ff6b6b 40deg, #ffd93d 90deg, #6bcb77 140deg, #4d96ff 190deg, #b86bff 240deg, transparent 290deg, transparent 360deg ); animation: rotate 3s linear infinite; filter: blur(12px); opacity: 0.8; z-index: -3; } /* 内层遮罩,盖住中间区域 */ .glow-btn .inner { position: absolute; inset: 2px; border-radius: 12px; background: #1a1a2e; z-index: -1; } .glow-btn span { position: relative; z-index: 1; } @keyframes rotate { to { --angle: 360deg; } } /* 悬停加速 */ .glow-btn:hover::before, .glow-btn:hover::after { animation-duration: 1.2s; } .glow-btn:hover { transform: translateY(-2px); } </style> </head> <body> <button class="glow-btn"> <span class="inner"></span> <span>点击开始</span> </button> </body> </html>

这段代码里几个关键点值得单独说。isolation: isolate是为了创建一个独立的层叠上下文,避免z-index: -1的伪元素跑到body背景后面去——这个坑我踩过,当时光带死活不显示,排查了半天才发现是层叠上下文的问题。inset: -3px让光带比按钮大3像素,这样光带边缘会露出来形成"描边"效果。内层.inner用inset: 2px盖住中间,只留出1像素左右的缝隙让光带透出来。

3.2 参数调优:颜色、速度、模糊半径怎么定

代码能跑起来只是第一步,真正决定效果好不好看的是参数。我调了大概二十多版才找到比较满意的组合,这里把经验值分享出来。

颜色停靠点的分布。上面代码里六个颜色分别停在40、90、140、190、240度,间隔50度左右,加上首尾的transparent,整圈360度里大约有250度是亮的。如果颜色停靠点太密,光带会显得"糊"在一起;太疏则会出现明显的色块断层。我的经验是每个颜色间隔45到60度比较舒服,颜色数量控制在4到6个。

动画时长。3秒一圈是默认值,这个速度下光带流动感明显但不至于晃眼。如果按钮尺寸比较大(比如宽度超过300px),可以适当放慢到4秒,因为光带跑过的距离变长了,同样的角速度看起来会更快。悬停时加速到1.2秒,这个反差能给出明确的交互反馈。

模糊半径。filter: blur(12px)是配合inset: -3px的。如果光带溢出更多(比如inset: -6px),模糊半径也要相应加大到18px左右,否则发光会显得"薄"。但要注意,blur值越大,GPU的合成开销越高,在低端设备上容易掉帧。我实测下来,12px在大多数手机上还能保持60fps,超过20px就开始有压力了。

参数推荐值调整方向性能影响
颜色数量4-6个越多越花哨,越少越干净无
动画时长3s(悬停1.2s)越短越快,越长越优雅无
模糊半径12px越大发光越柔,越小越锐利高
光带溢出3px越大描边越粗无
内层缩进2px越大光带越细无

3.3 一个容易忽略的细节:border-radius的继承

伪元素的border-radius必须和按钮保持一致,否则光带在圆角处会露出直角。我一开始写的是固定值12px,后来把按钮改成50px做胶囊形,光带就穿帮了。正确做法是用border-radius: inherit,让伪元素自动继承父元素的圆角值。这个细节很小,但在做响应式或者主题切换的时候特别重要。

4. 踩坑实录:流光按钮最容易翻车的五个地方

代码写出来能跑,和效果稳定可靠,中间隔着好几条鸿沟。下面这五个问题是我在实际项目中真实遇到过的,每一个都花了不少时间排查,写出来帮你省点功夫。

4.1 光带闪烁:合成层与动画属性的冲突

第一个坑最诡异:光带在旋转过程中会不定期地"闪一下",像是掉了一帧。一开始我以为是动画时长设得不对,调了半天没用。后来用DevTools的Layers面板一看,发现光带所在的伪元素在动画过程中反复被提升和降级合成层,导致重绘。

根因是filter: blur()和conic-gradient同时作用在一个元素上时,浏览器对是否要把它提升为独立合成层犹豫不决。解决办法是显式地给伪元素加上will-change: transform或者transform: translateZ(0),强制它待在合成层里:

.glow-btn::after { will-change: transform; /* 或者 */ transform: translateZ(0); }

加上之后闪烁立刻消失了。但要注意,will-change不能滥用,用多了会吃内存。这里只加在发光层上就够了,光带本体不需要。

4.2 移动端掉帧:blur是性能杀手

在桌面浏览器上跑得好好的效果,一到手机上就卡成幻灯片。用性能面板一测,发现filter: blur(12px)在移动端GPU上开销极大,尤其是当模糊区域跟着动画每帧重绘的时候。

我的优化方案是:移动端降低模糊半径到6px,同时把发光层的opacity从0.8降到0.5。视觉上发光会弱一些,但流畅度回来了。更好的做法是用媒体查询做分级:

@media (max-width: 768px) { .glow-btn::after { filter: blur(6px); opacity: 0.5; } }

如果对性能要求更极致,可以干脆在移动端去掉发光层,只保留光带本体。实测下来,光带本身的conic-gradient动画在移动端是很流畅的,瓶颈只在blur。

4.3 Safari的@property兼容性陷阱

前面提过,@property在Safari 16.4之前是不支持的。如果你的用户里有相当比例的老Safari,整个动画会直接失效——光带会静止不动,因为--angle无法插值。

降级方案有两个思路。一是用@supports做特性检测,不支持的时候换成一个静态的渐变边框:

@supports not (background: conic-gradient(from 0deg, red, blue)) { .glow-btn::before { background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff); animation: none; } }

二是用JavaScript做兜底,检测到不支持就动态加一个class,用background-position动画模拟流光。但这就违背了"纯CSS"的初衷,看你的项目取舍。

4.4 颜色在深色背景上"发灰"

这个问题比较隐蔽:同样的渐变色,放在白色背景上很鲜艳,放在深色背景上却显得灰蒙蒙的。原因是深色背景降低了整体亮度对比,人眼对色彩的感知会变弱。

解决办法是提高颜色的饱和度和明度。比如把#ff6b6b换成#ff4757,把#4d96ff换成#3742fa。另外可以在发光层上加一点mix-blend-mode: screen,让光带和背景做滤色混合,颜色会更"透":

.glow-btn::after { mix-blend-mode: screen; }

但mix-blend-mode也会触发额外的合成开销,移动端慎用。

4.5 按钮文字被光带"吃掉"

最后一个坑是布局层面的:光带用z-index: -1压在按钮背景下面,但按钮文字如果没有显式设置z-index,在某些浏览器里会被光带盖住。尤其是当按钮用了overflow: hidden的时候,层叠顺序更容易出问题。

稳妥的做法是给文字包一层span,显式设置position: relative; z-index: 1。这样无论光带怎么折腾,文字始终在最上层。这个细节在Chrome里可能看不出来,但在Firefox和部分安卓浏览器上就会暴露。

5. 进阶玩法:让流光按钮适配更多场景

基础效果跑通之后,可以基于同一套机制做很多变体。下面这几个是我在实际项目里用过的,每个都只需要改几行代码。

5.1 不同形状:从圆角矩形到胶囊、圆形

光带效果对形状的适应性很好,只要把border-radius改掉就行。胶囊形用border-radius: 999px,圆形按钮用border-radius: 50%加等宽高。但要注意,圆形按钮的光带用conic-gradient会显得"转得太快",因为同样的角速度下,圆周越短线速度越慢。可以适当放慢动画时长到4到5秒。

5.2 不同配色:从彩虹到单色呼吸

不是所有场景都适合彩虹色。后台系统里我更喜欢用单色流光,比如蓝色系:transparent → #4d96ff → #6bcbff → transparent。这种配色更克制,不会抢内容的注意力。做法是把conic-gradient里的颜色停靠点减少到两三个,光带会变成一道"扫光"。

5.3 触发方式:从常驻到hover、focus、active

常驻动画在页面上放多了会显得吵。更好的做法是默认静止,只在交互时触发。把animation从伪元素上移到:hover状态:

.glow-btn::before { animation: none; } .glow-btn:hover::before, .glow-btn:focus-visible::before { animation: rotate 2s linear infinite; }

focus-visible是为了键盘用户,保证无障碍访问。这个细节很多流光按钮的教程都会忽略,但实际项目里很重要。

5.4 和加载状态结合:流光作为进度指示

流光效果天然适合做"加载中"的视觉反馈。把光带的速度和加载进度关联起来(通过CSS变量动态调整animation-duration),就能做出一个不需要额外进度条的加载按钮。这个用法我在一个文件上传组件里试过,用户反馈比传统的转圈图标更有质感。

6. 写在最后:关于CSS动效的一点个人体会

做前端这些年,我越来越觉得CSS动效的价值不在于"炫",而在于"准"。一个流光按钮,如果只是随便找个代码复制进去,大概率会出现颜色不搭、速度不对、移动端卡顿的问题。真正花时间的地方,是把每个参数调到和你的产品调性匹配——后台系统要克制,营销页面可以张扬,工具类应用要快而轻。

@property这个特性刚出来的时候,我觉得它只是个语法糖。但用它做过几个项目之后,发现它其实改变了CSS动画的思维方式:以前做角度动画只能靠transform: rotate去转整个元素,现在可以直接操作渐变的角度,让"光"这个概念在CSS里有了更自然的表达。流光按钮只是它最直观的应用之一,同样的机制还能做进度环、雷达扫描、呼吸灯等等。

如果你跟着这篇文章把代码跑通了,建议再试试改改颜色停靠点的位置,观察光带形状的变化。这个过程中你会对conic-gradient的角度分布有更直觉的理解,以后遇到类似的环绕动效,基本可以做到心里有数、手上不慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询