news 2026/9/19 3:14:41

CSS圆锥渐变实现流光边框动画:conic-gradient与@property实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CSS圆锥渐变实现流光边框动画:conic-gradient与@property实战指南

前几天接了一个视觉稿,卡片四周要带一圈会流动的彩色渐变边框,设计师原话是“就一个流光描边,一下午能上吧”。我盯着那个匀速转圈的亮斑看了几秒,第一反应是交给 Canvas 或者 Lottie,但冷静下来之后意识到,这类效果用 CSS 就能做得非常干净:圆锥渐变(conic-gradient)负责生成沿角度分布的光源,mask 把光裁成一条边框,再让角度变量动起来,就成了流光效果的边框样式。这篇文章把我从第一版能出效果的“伪边框”写法,到顺滑动画版本,再到兼容性和性能取舍的完整过程都写下来,适合已经写过基础 CSS、想在按钮或卡片上做高级视觉的同学直接参考。

1. 圆锥渐变为什么天然适合做“流动的光”

1.1 conic-gradient 其实就是一张“角度调色盘”

linear-gradient 是沿一条直线方向渐变,radial-gradient 是从中心向外扩散,而 conic-gradient 的坐标系是“绕圆心转一圈”。你可以把它想象成一张钟表盘:0 度相当于 12 点方向,90 度是 3 点方向,180 度是 6 点方向,360 度又回到 12 点方向。

background: conic-gradient(from 0deg, #ff0080, #7928ca, #ff0080);

这段代码的意思是:从 0 度开始,颜色从粉色过渡到紫色,再过渡回粉色,整个渐变在圆盘上形成一个环形色带。如果把它放到一个正方形元素里,会看到明显的扇形过渡;如果把圆角矩形裁掉中间区域,只保留边缘,就是一条彩色描边。这正是流光边框的基础素材。

圆锥渐变最特别的地方在于它的起点和终点是天然闭合的。线性渐变从左边到右边,结束之后没有“接缝”的概念;但圆锥渐变绕一圈回来,只要首尾颜色一致,就能形成无缝循环。这对“流动”效果是决定性的,因为流光本质上就是一个周期循环的视觉信号,圆锥渐变天生就符合这个模型。

1.2 流光效果需要的三个视觉要素

把“流光”拆开看,其实就三个要素:一个集中的光源、一个可变化的角度起点、一条窄边框作为载体。

很多新手第一次看到 conic-gradient 做边框,会直接写满六个颜色,让整圈边框不断变色。这种效果确实在转,但视觉上是“彩虹在旋转”,不是“一道光在扫过”。真正的流光效果应该像手电筒照过墙壁:大部分区域是暗的,只有一小段角度范围内是亮的,而且亮斑会随着时间移动。

所以更合理的渐变结构是让亮部集中在一小段角度里。

background: conic-gradient( from var(--angle), transparent 0deg, rgba(255, 255, 255, 0.9) 8deg, #22d3ee 22deg, #a78bfa 80deg, transparent 140deg, transparent 360deg );

这段代码里,0 度到 8 度之间非常亮,8 度到 22 度之间是主色,22 度到 140 度逐渐衰减,140 度之后就完全透明,直到转回起点。这样画面上只会看到一段窄光带,再加上一段较长的余晖,视觉上就是“一道光在描边”,而不是整圈颜色均匀旋转。

角度起点就是控制这束光位置的开关。只要让--angle从 0 度匀速变到 360 度,光带就会沿着边框完成一圈流动。这个“角度变量”是整套实现的核心,后面动画部分会重点讲。

2. 第一版实现:双层背景加 mask 裁出真正的渐变边框

2.1 先别急着上 mask:双层背景的快速方案

最直观的做法是用border: 2px solid transparent占住边框宽度,然后叠两层背景:一层放在 padding-box,作为卡片内部的底色;另一层放在 border-box,作为圆锥渐变的辖区。

.quick-border { border: 2px solid transparent; border-radius: 16px; background: linear-gradient(#0f1220, #0f1220) padding-box, conic-gradient(from 0deg, #22d3ee, #a78bfa, #f472b6, #22d3ee) border-box; }

为什么这样能形成边框?因为padding-box的底色层刚好覆盖从边框内缘到内容区的全部区域,而border-box的渐变层覆盖到边框外缘。两层叠加之后,内部区域被不透明的深色层盖住,只有最外圈 2px 的边框区域露出圆锥渐变色。

这个方案的优点是结构简单,不需要伪元素,也不用管 mask-composite 的兼容性。缺点是内部背景必须是不透明的,否则圆锥渐变会从半透明背景下面透出来,整个卡片都会花掉。如果设计师要的是半透明毛玻璃卡片,或者背景需要透出页面底图,这个方案就不能用。

2.2 透明内芯版本:用 mask 裁出边框环

真正的通用方案是用伪元素承载渐变,再用 mask 把伪元素的中间区域“挖空”,只留下边框环。

<div class="card card--glow"> <div class="card__body"> 这里是卡片内容 </div> </div>
.card--glow { --border-size: 2px; --radius: 18px; position: relative; border-radius: var(--radius); } .card--glow::before { content: ""; position: absolute; inset: 0; z-index: 2; border-radius: inherit; padding: var(--border-size); background: conic-gradient(from var(--angle), #22d3ee, #a78bfa, #f472b6, #22d3ee); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude; pointer-events: none; }

mask 在这里做了两件事:第一层linear-gradient(#fff 0 0) content-box只让内容区域可见,第二层默认的 border-box 让整个伪元素区域可见,mask-composite: exclude取两者的差集,也就是只保留 border 区域那 2px 的环。

伪元素外层仍然有一层 padding,但内容是空字符串,所以 content-box 区域是透明空气。渐变背景虽然覆盖了整个矩形,但 mask 把中间裁掉,最终用户只看到一圈渐变色边框。

有一个很关键的坑必须提醒:mask 是作用在整个元素上的,如果直接把文字内容放进这个伪元素,文字也会被裁掉。因此实际内容必须放在.card__body这类独立子元素中,层级上让内容在伪元素下方,视觉上伪元素作为最外层的“描边覆盖层”存在。

2.3 为什么有人做出来边框里外全是渐变色

最常见的问题就是内容和渐变层放在同一个元素上,或者 mask 层叠顺序没写对。如果你看到整个元素都被渐变色填满,大概率是 mask-composite 没生效。

现代浏览器推荐标准写法mask-composite: exclude,但 Safari 和旧版 Chrome 需要-webkit-mask-composite: xor。两种写法的语义在“排除”这个场景下是等价的,但属性名和取值都不同,所以代码里两个都要留:

-webkit-mask-composite: xor; mask-composite: exclude;

另一个容易翻车的地方是圆角。外层border-radius: 18px,边框宽度 2px,那么内层内容的圆角大约是18px - 2px = 16px,否则深色内芯的直角会戳出边框。我在实际项目里经常看到有人外层圆角写 20px,内层内容圆角也写 20px,结果边框在四个角上突然变宽,视觉上像接口处鼓起一个包,非常明显。

3. 让光动起来:用 @property 驱动角度而不是折腾 background

3.1 为什么不直接改 background-position 或 transform

拿到静态渐变边框后,下一步就是让它动。我见过有人用 animation 去改background-position,对线性渐变可能还凑合,但圆锥渐变根本没有 x/y 方向的概念,改 background-position 只会让整张渐变背景在盒子里平移,光带绕着边界走的效果完全出不来。

还有人想到旋转整个元素或伪元素。这个方法在正方形元素上勉强能用,但在长宽比很大的卡片上问题很明显:元素几何中心和圆形渐变中心不重合,旋转之后光带速度一会儿快一会儿慢;而且伪元素旋转到非整数角度时,四个角会露出空白,必须把伪元素放大 150% 以上再配 overflow 裁剪,整体方案会变得很难控制。

所以最合理的做法不是“移动”或者“旋转”,而是让圆锥渐变的起始角度from var(--angle)本身随时间变化。角度从 0 度平滑增长到 360 度,视觉效果就是光带沿着边框跑完一整圈。

3.2 注册自定义属性 --angle 的完整姿势

要让 CSS 动画能够平滑修改一个自定义属性,必须用 @property 先把这个变量注册成<angle>类型,否则浏览器不知道这个变量是角度,动画会从 0 度直接跳到 360 度。

@property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; } .card--glow::before { background: conic-gradient( from var(--angle), transparent 0deg, rgba(255, 255, 255, 0.95) 8deg, #22d3ee 22deg, #a78bfa 80deg, transparent 140deg, transparent 360deg ); animation: border-flow 4s linear infinite; } @keyframes border-flow { to { --angle: 360deg; } }

@property声明里最关键的是syntax: "<angle>"。只要注册成角度类型,浏览器在做动画插值时就会把它当作角度处理,从 0 到 360 生成连续帧;如果不注册,浏览器只会把--angle当成普通字符串变量,动画极可能只有首尾两帧,中间完全丢失。这一点和 CSS 变量常见的字符串拼接逻辑很不一样,很容易踩坑。

3.3 调节“流光”观感的几个旋钮

光带快不快、亮不亮、像不像“一道光”,主要由四个参数决定。

第一个是animation-duration。4 到 6 秒适合卡片,2 到 3 秒适合按钮,头像可以稍微快一点,3 秒左右比较有活力。

第二个是亮部区域宽度。在 conic-gradient 中,亮斑占的角度越窄,光束越锐利;占的角度越宽,越接近大面积彩色漫反射。想要高级感,我建议亮部控制在 8 到 20 度之间,余晖可以拖到 80 到 120 度。

第三个是颜色 stop 的密度。同一个渐变里如果所有颜色集中在一半角度内,另一半完全透明,视觉上就会产生明显的“一段亮一段暗”对比。这种对比是流光感的核心,为了让效果更明显,我会在亮斑后面加一个纯白透明度的 stop:

transparent 0deg, rgba(255, 255, 255, 0.95) 8deg, #22d3ee 16deg, transparent 120deg

第四个是动画的 timing-function。流光必须用linear,如果使用easeease-in-out,亮斑会在起点和终点附近减速,看着像卡顿。

参数卡片场景按钮场景头像场景
动画时长6s2.5s3s
亮斑角度12deg18deg8deg
光带数量2 个1 个1 个
推荐方向顺时针hover 时反向顺时针

4. 进阶玩法:辉光、不同元素形态和动效降级

4.1 给边框加一层柔光

纯边框流光在某些深色背景下会显得边缘太锐利,像贴了一道塑料条。这时候可以叠加一个模糊的辉光层,让光带周围有一圈柔和泛光。

辉光层的做法和主边框层几乎一样,区别是它要放在主边框层下面,并且加 filter blur。

.card--glow::after { content: ""; position: absolute; inset: 0; z-index: 0; border-radius: inherit; padding: calc(var(--border-size) + 6px); background: conic-gradient(from var(--angle), #22d3ee, #a78bfa, transparent 60%); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude; filter: blur(12px); opacity: 0.5; pointer-events: none; }

这里我把辉光层的 padding 加大到border-size + 6px,让裁出来的“边框环”更宽,再配合 blur 模糊,视觉上就形成了一圈从边框向外扩散的光晕。要注意父容器不能设置 overflow: hidden,否则 blur 产生的溢出部分会被裁掉,辉光效果大打折扣。

4.2 头像、按钮、Tab 选中态的不同改造

头像和卡片的实现逻辑相同,但有几个细节必须调整。第一,圆角要改成 50%,并且内层内容也要是圆形;第二,如果头像尺寸小,比如 48px 的头像,边框宽度最好降到 2px 以下,否则会盖住太多头像内容;第三,小尺寸元素上光带角度不宜太宽,8 度左右刚好,太宽会变成整个圆圈都在发光。

按钮场景我喜欢把流光放在 hover 态触发。原本是静态渐变边框,鼠标移入时才开始流动,这样不会在列表页里造成大范围视觉噪音。具体做法是默认不给伪元素加 animation,hover 时再开启:

.btn--glow::before { animation: none; } .btn--glow:hover::before { animation: border-flow 1.2s linear infinite; }

需要注意 animation 从无到有会有一个“启动”过程,第一次触发时可能会亮斑从当前位置突然出现。为了避免突兀,可以给 animation 加一个animation-delay: 0s,并且保持渐变结构一致,用户感知上就会是一个平滑出现的动态描边。

Tab 选中态更适合静态渐变,或者用极慢的流光作为背景辅助。因为 Tab 是信息导航,用户视线需要快速定位,一旦有强烈动效持续闪烁,会严重影响扫视效率。我的经验是:功能性控件动效越克制越好,装饰性大卡片可以稍微放开。

4.3 prefers-reduced-motion 与静态兜底

动效不是所有人都能接受,尤其是前庭功能敏感的用户,持续环绕运动的光斑会让人头晕。CSS 里可以用prefers-reduced-motion媒体查询做无障碍适配:

@media (prefers-reduced-motion: reduce) { .card--glow::before { animation: none; } }

静态状态下,conic-gradient 边框仍然保留,只是光斑不转。这样既不丢失视觉层次,也不会强迫用户长时间观看高动态画面。把这条规则加到任何动效组件里,是提升质量下限成本最低的一步。

5. 真机调试时踩过的坑和最终验证过的模板

5.1 Safari 和旧浏览器的兼容性处理

这套实现最大的兼容性变量来自两部分:@propertymask-composite

@property在现代 Chromium 和较新版本的 Safari/Firefox 中都能用,但如果目标环境是较旧的 Safari 15 或更低版本,@property可能没有被支持,动画会退化成只有首尾两帧。表现就是边框静态不流动,但不会报错,页面也不会崩溃。

如果一定要在旧环境里让光流动起来,可以用 JS 每帧更新 CSS 变量。注意这时候 CSS 里的 animation 必须关掉,否则两套逻辑会同时写同一个变量,出现闪烁和抖动。

const el = document.querySelector('.card--glow'); let angle = 0; function tick() { angle = (angle + 0.8) % 360; el.style.setProperty('--angle', angle + 'deg'); requestAnimationFrame(tick); } tick();

mask-composite 也有版本差异。写的时候把-webkit-mask-composite: xormask-composite: exclude都写上,并且把 Webkit 前缀版本放在标准版本前面。

5.2 光带“忽快忽慢”的真相

我调这个效果时遇到过一种非常隐蔽的问题:光带看起来在某个角度区间明显变慢,过了某个角度又突然加速。排查下来主要有两个原因。

第一个原因是 timing-function 用了非 linear。默认的 ease 会让动画在开始和结束时减速,如果光带正好跑一圈,起点终点附近自然会慢。解决方法是明确写成linear

第二个原因是伪元素的几何中心和渐变圆心不重合。这种情况常见于用 transform 旋转伪元素代替角度动画,元素越长,速度不均匀越明显。卡片宽高比大到 2:1 时,光带在长边和短边上运动速度差异肉眼可见。所以我最终只使用 @property 角度动画,不推荐用 transform 旋转伪元素的方案来模拟。

5.3 可直接抄走的最佳实践模板

下面是我在项目中验证过的最终方案:外层卡片负责定位和圆角,伪元素负责承载动态渐变边框,内层子元素负责承载真实内容。所有关键参数都用 CSS 变量暴露出来,换色或者调速不需要翻动关键帧。

<div class="card-glow"> <div class="card-glow__inner"> <h3>流光会员卡</h3> <p>conic-gradient 实现动态渐变边框</p> </div> </div>
@property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; } .card-glow { --border-size: 2px; --radius: 20px; --c1: #22d3ee; --c2: #a78bfa; --c3: #f472b6; --duration: 5s; position: relative; border-radius: var(--radius); isolation: isolate; } .card-glow::before { content: ""; position: absolute; inset: 0; z-index: 2; border-radius: inherit; padding: var(--border-size); background: conic-gradient( from var(--angle), transparent 0deg, rgba(255, 255, 255, 0.95) 8deg, var(--c1) 22deg, var(--c2) 80deg, var(--c3) 140deg, transparent 190deg, transparent 360deg ); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude; pointer-events: none; animation: border-flow var(--duration) linear infinite; } .card-glow__inner { position: relative; z-index: 1; border-radius: calc(var(--radius) - var(--border-size)); background: #0f1220; padding: 24px; } @keyframes border-flow { to { --angle: 360deg; } } @media (prefers-reduced-motion: reduce) { .card-glow::before { animation: none; } }

调试时有个小技巧:把--duration临时改成 1s,光带运动速度会非常快,你一眼就能看出亮斑是否沿着边框走、方向对不对、四角有没有露出异常;确认无误后再改回 5s 或者 6s。我自己做这类装饰效果时会先在小按钮上试,确认光带宽度和颜色不抢内容主次,再决定要不要铺到整张大卡片上。光带越窄、颜色越克制,出来的质感越接近设计稿,而不是满脸跑马灯。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 3:14:25

大华DH-EVS7064S-R网络视频存储服务器部署与RAID/iSCSI配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 3:14:22

VC Spyglass Lint工作流实战:从CDC报告到RTL代码收敛

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 3:13:14

齿轮-轴-轴承系统含间隙非线性动力学的Matlab仿真指南

去年做齿轮箱早期故障诊断时&#xff0c;甲方那边反馈最典型的一个现象是&#xff1a;设备在某一转速区间内振动异常刺耳&#xff0c;换挡或加减速时变速箱体有“咔哒”异响&#xff0c;停机拆检却发现齿轮没有明显点蚀或断齿&#xff0c;轴承也无明显磨损痕迹。这个问题让不少…

作者头像 李华
网站建设 2026/9/19 3:09:16

MPX4115智能压力检测:从ADC采样到标定温补与自诊断

简介&#xff1a;这份基于MPX4115传感器的智能压力检测PDF文档&#xff0c;面向电子信息、自动化及测控专业的课程设计、毕业设计学习者&#xff0c;围绕数字气压计软硬件实现展开。内容以MPX4115气压传感器采集大气压并输出模拟电压为主线&#xff0c;经V/F转换模块变为数字脉…

作者头像 李华