CSS圆锥渐变实现流光边框动画:conic-gradient与@property实战指南 📅 发布时间:2026/9/19 3:15:03 👁 浏览次数: 前几天接了一个视觉稿卡片四周要带一圈会流动的彩色渐变边框设计师原话是“就一个流光描边一下午能上吧”。我盯着那个匀速转圈的亮斑看了几秒第一反应是交给 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 classcard card--glow div classcard__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 驱动角度而不是折腾 background3.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 的密度。同一个渐变里如果所有颜色集中在一半角度内另一半完全透明视觉上就会产生明显的“一段亮一段暗”对比。这种对比是流光感的核心为了让效果更明显我会在亮斑后面加一个纯白透明度的 stoptransparent 0deg, rgba(255, 255, 255, 0.95) 8deg, #22d3ee 16deg, transparent 120deg第四个是动画的 timing-function。流光必须用linear如果使用ease或ease-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 态触发。原本是静态渐变边框鼠标移入时才开始流动这样不会在列表页里造成大范围视觉噪音。具体做法是默认不给伪元素加 animationhover 时再开启.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 和旧浏览器的兼容性处理这套实现最大的兼容性变量来自两部分property和mask-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: xor和mask-composite: exclude都写上并且把 Webkit 前缀版本放在标准版本前面。5.2 光带“忽快忽慢”的真相我调这个效果时遇到过一种非常隐蔽的问题光带看起来在某个角度区间明显变慢过了某个角度又突然加速。排查下来主要有两个原因。第一个原因是 timing-function 用了非 linear。默认的 ease 会让动画在开始和结束时减速如果光带正好跑一圈起点终点附近自然会慢。解决方法是明确写成linear。第二个原因是伪元素的几何中心和渐变圆心不重合。这种情况常见于用 transform 旋转伪元素代替角度动画元素越长速度不均匀越明显。卡片宽高比大到 2:1 时光带在长边和短边上运动速度差异肉眼可见。所以我最终只使用 property 角度动画不推荐用 transform 旋转伪元素的方案来模拟。5.3 可直接抄走的最佳实践模板下面是我在项目中验证过的最终方案外层卡片负责定位和圆角伪元素负责承载动态渐变边框内层子元素负责承载真实内容。所有关键参数都用 CSS 变量暴露出来换色或者调速不需要翻动关键帧。div classcard-glow div classcard-glow__inner h3流光会员卡/h3 pconic-gradient 实现动态渐变边框/p /div /divproperty --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。我自己做这类装饰效果时会先在小按钮上试确认光带宽度和颜色不抢内容主次再决定要不要铺到整张大卡片上。光带越窄、颜色越克制出来的质感越接近设计稿而不是满脸跑马灯。