资讯动态

纯CSS霓虹聚光灯特效:text-shadow与渐变裁剪的完整实现

发布时间:2026/10/20 14:48:56 来源:尧图企业网站定制
做前端这些年我越来越觉得“CSS特效-霓虹聚光灯效果”是性价比极高的一种玩法它不需要任何图片素材不依赖 Canvas 绘图甚至可以不写一行动画库代码就靠几个背景渐变加一束会动的光就能把一行普通标题变成暗夜里发光的霓虹灯管。第一次看到成品的时候我真有种“这些代码是怎么做到的”的错愕感。这篇文章我不会只丢一个成品 Demo而是从拆解原理开始一路讲到自动扫光、鼠标跟随、色调流动再到兼容性和我踩过的几个典型坑。适合刚接触 CSS 特效的朋友照着实操也适合已经有基础但想优化细节的人直接拿走参数。1. 先把霓虹聚光灯拆成三层视觉模型1.1 “霓虹、追光、裁剪”分别解决什么一个能打的霓虹聚光灯效果表面看是“彩色光晕移动光斑”但拆开之后其实是三个独立视觉问题。第一个是霓虹感。真实霓虹灯之所以漂亮是因为灯管内部的气体在高压下发亮光从灯管壁透出来视觉上有一个很亮的内芯外面还裹着一层散开的雾状光晕。我在 CSS 里模拟它主要靠text-shadow的多层叠加文字本体充当灯管内芯多层阴影交错向外扩散模拟光从笔画边缘泄漏出去的感觉。这个阶段的核心是“内芯要亮、光晕要散、浓度要递减”。第二个是聚光灯行为。一束追光的特征是它有一个边界相对清晰的圆形光斑中心亮度极高向外迅速衰减并且它会移动。静止的光只能叫打光动起来才叫追光。在 CSS 里这个光斑其实就是一个径向渐变radial-gradient光斑的移动可以交给动画或者鼠标坐标。第三个是约束问题。如果直接把一个圆形渐变画在区块背景上它就是一坨光和文字没有任何互动。要做出“追光照亮霓虹字”的感觉必须让这束光只在文字笔画的范围内露出来。约束靠background-clip: text解决它的作用是把背景图裁剪到文字形状内。没有这一步光线扫过去观众只会觉得背景上有个手电筒而不会觉得文字被点亮。这三步单拆出来都不难难的是顺序。顺序一乱最常见的结果就是光晕莫名其妙消失或者光斑把整个背景照得通亮。1.2 为什么最终选纯CSS而不是图片和Canvas我做一个特效前习惯先把候选方案列出来比一比免得写着写着发现路子不对。图片方案最直接把一张霓虹灯文字的 PNG 或 GIF 贴上去加个遮罩扫光就是成品。但后果很现实文字不能实时改换文案要重新出图小尺寸下表不清晰而且一旦涉及版权和响应式维护成本瞬间上去了。图片方案适合一次性视觉稿不适合需要灵活更新的页面。Canvas 方案上限确实高可以做粒子组成的霓虹、可以做不规则光晕但对一个标题来说代码量、事件处理、响应式适配都是负担。还有一个容易忽略的问题Canvas 里的文字对屏幕阅读器不友好复制、选中、抓取都麻烦。SVG 滤镜的feGaussianBlur做光晕也很能打但排版不如 HTML 灵活做多行文字、换行、点击事件时很别扭。纯 CSS 是我的最终选择。文字的载体还是真正的 DOM 文本节点爬虫能读到、用户能复制、字号响应式随便调光晕和追光分别用text-shadow、background-image两个“图层”承担动画成本低性能也可控。对“一行大标题”这种典型应用它几乎是最优解。1.3 贯穿全文的分层结构先记住这一句后面所有版本都逃不开一个分层模型我用一句话概括底层负责发光上层负责追光裁剪负责把关。服务端至少有两个文字层通常用两个span放在同一个容器里。底层文字颜色不透明靠text-shadow输出霓虹光晕这是“灯管本体”。上层文字设置background-image为径向渐变光斑再用background-clip: text把光斑裁剪成文字轮廓同时color: transparent把文字本身让出来让背景图露出来。理解这个模型之后你看到的所有花哨效果本质上都是在底层换光晕颜色、在上层换光斑形态、在动画里换运动方式。杂技再多台子就这一座。2. 静态霓虹入门text-shadow才是真正的舞台灯2.1 一条条text-shadow如何模拟灯管光晕先把最基础的静态效果搭出来让文字先“亮”起来。我用的是一个很典型的霓虹参数组合.neon-static { font-size: clamp(3rem, 10vw, 7rem); font-weight: 800; color: #f5f5ff; background-color: #0a0a12; text-shadow: 0 0 6px rgba(255, 255, 255, 0.9), 0 0 12px rgba(255, 255, 255, 0.7), 0 0 25px rgba(0, 240, 255, 0.55), 0 0 55px rgba(0, 240, 255, 0.4), 0 0 90px rgba(255, 0, 220, 0.35); }这里每一条 shadow 都不是随手填的。最前面的0 0 6px和0 0 12px是白色系目的是让笔画核心区域看起来更亮像灯管通电瞬间发白光。中间的 25px、55px 是青色系负责向外扩散出主光晕。最后一条 90px 的洋红色是让你感觉光有一层淡淡的漫射余光。半径为什么要按这种“6、12、25、55、90”的节奏走因为光衰减不是线性的近处应该密一点远处稀疏一点。按一个近似倍增的序列视觉上光晕浓度会平滑递减不会出现一圈圈突兀的“亮度断层”。如果四条阴影全部用同一个半径效果会像一个厚实的描边而不是光晕。底色我选的是近似深蓝黑的#0a0a12而不是纯黑。纯黑会让霓虹周围完全“死”掉深一点带冷调的氛围色反而可以让光晕显得更通透。字体最好选笔画粗、字距略宽的光效细节才有地方施展。2.2 最容易翻车的基础结构为什么要叠两层文字很多人第一次做聚光灯版时会直接在一个元素上同时做霓虹光晕和扫光结果多半会翻车。我先说一个准确的坑text-shadow如果省略了颜色值它会自动使用当前文字的color。一旦你为了做背景裁剪把color设成transparent光晕就会跟着变成透明的。就算你用text-shadow时显式写了白色光晕仍然会沿文字轮廓发光而这时候背景里的扫光斑也被盖在下面整体发灰、发虚追光的效果会被吃掉。所以我的固定做法是叠两层结构。底层拿不透明文字加光晕上层拿透明文字接收背景光斑各干各的活互不干扰。div classneon span classneon__text aria-hiddentrueNEON/span span classneon__spotNEON/span /div.neon { position: relative; font-size: clamp(3rem, 10vw, 7rem); font-weight: 800; font-family: Trebuchet MS, Arial Black, sans-serif; background-color: #0a0a12; padding: 0.5em 1em; } .neon__text { color: #f5f5ff; text-shadow: 0 0 6px rgba(255, 255, 255, 0.9), 0 0 12px rgba(255, 255, 255, 0.7), 0 0 25px rgba(0, 240, 255, 0.55), 0 0 55px rgba(0, 240, 255, 0.4), 0 0 90px rgba(255, 0, 220, 0.35); } .neon__spot { position: absolute; inset: 0; background-image: radial-gradient( circle 10rem at 12% 50%, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.18) 30%, transparent 60% ); -webkit-background-clip: text; background-clip: text; color: transparent; }注意几个细节.neon__spot和.neon__text必须在同一个字体、字号、行高下否则两层文字的位置对不齐。.neon上加position: relativespot层用position: absolute; inset: 0去对齐容器。父容器要留一点padding否则光斑扫到文字边缘时会贴着裁切边界显得很生硬。给光晕层的文字加aria-hiddentrue是合理的因为视觉上它是底层文字的副本没必要让屏幕阅读器再读一遍。这套结构里text-shadow造成的是一种“发光”的假象而光斑是额外叠上去的“被照亮”区域。它和真实世界不完全一样但观众眼睛不会去追究物理逻辑只要亮与暗的对比符合直觉就能骗过大脑。2.3 用background-position实现低门槛扫光在引入高级动画能力之前先介绍一个低门槛的扫光实现。这个方案兼容性最好不需要注册新属性原理也直观把光斑画在一张较大的背景图上然后移动背景图的位置。.neon__spot { position: absolute; inset: 0; background-image: radial-gradient( circle 10rem at center, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.1) 35%, transparent 65% ); background-size: 240% 100%; background-position: -70% 50%; background-repeat: no-repeat; -webkit-background-clip: text; background-clip: text; color: transparent; animation: bg-sweep 6s ease-in-out infinite alternate; } keyframes bg-sweep { from { background-position: -70% 50%; } to { background-position: 170% 50%; } }背景尺寸设成240%是为了给背景图在元素宽度内留出可移动的余量。初始位置在左侧-70%终点到右侧170%光斑就能完整地从左边界扫到右边界再反向扫回来。这个方案在旧浏览器里也能正常跑唯一的限制是光斑移动范围受背景尺寸约束想要更精细的曲线控制就要用到下一章的 CSS 变量方案。3. 自动扫光与鼠标跟随让聚光灯真正“打”在文字上3.1 用property做自动往返扫光的正确姿势如果希望光斑移动更精确、更自由推荐用自定义属性保存光斑中心坐标再配合property让这个变量可以被动画平滑插值。property --spot-x { syntax: percentage; inherits: false; initial-value: 10%; } .neon__spot { background-image: radial-gradient( circle 10rem at var(--spot-x) 50%, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.18) 30%, transparent 62% ); animation: spot-sweep 6s ease-in-out infinite alternate; } keyframes spot-sweep { from { --spot-x: 10%; } to { --spot-x: 90%; } }为什么选ease-in-out而不是linear真实世界的追光灯启动时有个加速过程到位前有个减速过程并会在尽头短暂停留。线性扫过去会像传送带缺乏这种“有生命”的感觉。这里要提醒一句property目前在较新的现代浏览器里支持得很好但部分旧内核浏览器不支持。不支持时自定义属性无法动画光斑会停留在初始值。后续我会讲降级方案先记住这个前提。3.2 鼠标跟随只靠两个CSS变量自动扫光的接单场景是装饰但交互场景里鼠标跟随式聚光灯更容易让人“玩起来”。实现思路很简单JS 只负责把鼠标位置换算成百分比写进--spot-x和--spot-y两个 CSS 变量绘制和过渡全都交给 CSS。先看 HTML 结构还是一样的双层文字div classneon interactive span classneon__text aria-hiddentrueNEON/span span classneon__spotNEON/span /divJS 部分非常短const target document.querySelector(.neon.interactive); target.addEventListener(pointermove, (e) { const rect target.getBoundingClientRect(); const x ((e.clientX - rect.left) / rect.width) * 100; const y ((e.clientY - rect.top) / rect.height) * 100; target.style.setProperty(--spot-x, x %); target.style.setProperty(--spot-y, y %); });我这里特意用pointermove而不是mousemove因为前者在触摸屏和触控笔上也能工作。坐标计算用getBoundingClientRect而不是事件对象里的offsetX/offsetY原因是offsetX是相对事件目标的如果鼠标移动到内部子元素上数值会发生跳动。用clientX减去容器左上角坐标得到的是相对容器自身的稳定值。CSS 侧只需要把光斑中心绑定到这两个变量上.neon.interactive .neon__spot { background-image: radial-gradient( circle 10rem at var(--spot-x, 30%) var(--spot-y, 50%), rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.4) 14%, rgba(255, 255, 255, 0.08) 38%, transparent 60% ); }var(--spot-x, 30%)后面的30%是默认值用来处理 JS 还没设置变量时的状态避免元素一亮相就是黑的。3.3 光斑形态与平滑跟随脱离“贴图感”的关键很多新手做鼠标跟随光斑做完后总觉得像“一块贴图跟着鼠标跑”问题通常出在光斑太圆、太均匀而且移动太生硬。真实聚光灯的光斑不是正圆。灯光从侧上方打下来落到文字表面会压成椭圆。我平时会用ellipse来替代circlebackground-image: radial-gradient( ellipse 12rem 9rem at var(--spot-x) var(--spot-y), rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.35) 20%, rgba(255, 255, 255, 0.06) 45%, transparent 65% );椭圆的长轴和短轴差距不用太大一点点倾斜感就足够真实。另一个提升质感的手段是让光斑带一点“灯光温度”纯白显得太硬偏暖的白更像演播室的追光。比如把中心设为rgba(255, 245, 230, 0.95)边缘色带一点淡黄。平滑跟随也有两段式方案。如果只是简单过渡可以在property注册变量后给 CSS 变量加transition鼠标停下时光斑会自己滑动过去。但如果鼠标快速来回甩transition的表现还是会有点“追不上”。想要更顺滑的阻尼感我一般直接用requestAnimationFrame做线性插值let currentX 30; let currentY 50; let targetX 30; let targetY 50; function tick() { currentX (targetX - currentX) * 0.12; currentY (targetY - currentY) * 0.12; target.style.setProperty(--spot-x, currentX %); target.style.setProperty(--spot-y, currentY %); requestAnimationFrame(tick); } tick(); target.addEventListener(pointermove, (e) { const rect target.getBoundingClientRect(); targetX ((e.clientX - rect.left) / rect.width) * 100; targetY ((e.clientY - rect.top) / rect.height) * 100; });系数0.12是一个经验值越大跟手越快越小惰性越强。做活动页时我偏爱0.1到0.14既有追踪感又不会显得迟钝。这个方案里JS 依然只是在“搬运坐标”不参与任何绘制计算所以性能很稳定。4. 色调流动与多光斑霓虹从“好看”到“惊艳”4.1 用色相变量让光晕自动变色静态霓虹再好看久了也会腻。我强烈建议把光晕颜色做成年久自动流动的状态让文字像真正的灯管一样在不同色温之间缓慢变化。做法是用一个--hue变量统一管理文字和光晕的色相然后注册成可动画的自定义属性。property --hue { syntax: number; inherits: false; initial-value: 300; } .neon-tone { --hue: 300; color: hsl(var(--hue) 100% 90%); text-shadow: 0 0 6px hsl(var(--hue) 100% 88%), 0 0 12px hsl(calc(var(--hue) - 10) 100% 75%), 0 0 25px hsl(calc(var(--hue) 20) 100% 60%), 0 0 55px hsl(calc(var(--hue) 40) 90% 55%); animation: hue-flow 10s ease-in-out infinite; } keyframes hue-flow { 0%, 100% { --hue: 300; } 50% { --hue: 200; } }这样文字会在紫色系和青色系之间来回漂移。注意hsl里我把饱和度拉得比较高但最终光晕半径很大的那几条透明度还是要由单独的 rgba 控制。实际上可以直接用hsl加透明度比如hsl(calc(var(--hue) 40) 90% 55% / 0.35)效果等同 rgba但颜色能跟着变量走。我给一个偏好色相变化周期不要低于 8 秒。太快会像酒吧旋转灯而不是霓虹。4.2 白光打底、彩色晕染双radial-gradient的叠加艺术如果说前面的光晕层负责氛围那扫描层的光斑也可以再做一层层次。一个细节到位的聚光斑应该是中心有白亮的灯芯外围再拖一层彩色弥散。我用两个radial-gradient叠加实现.neon__spot { background-image: radial-gradient( circle 8rem at var(--spot-x) var(--spot-y), rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.2) 22%, transparent 42% ), radial-gradient( circle 13rem at var(--spot-x) var(--spot-y), rgba(0, 230, 255, 0.28) 0%, transparent 60% ); }第一个渐变是“灯芯”半径小、中心亮度高第二个渐变是“光晕”半径大得多、透明度低。两个叠加之后光斑中心会呈现一种“白里透青”的灯箱感边缘像是被空气晕开。这个技巧尤其适合配合 4.1 的色相流动把第二个渐变的主色也换成变量聚光灯就会和文字的光晕一起变色整体统一又耐看。4.3 双追光交叉扫过的实现思路单一光斑做交互已经够用但如果是品牌页首屏这种需要更强视觉冲击的地方可以试试双追光交叉扫过。思路是在同一个background-image里放两个径向渐变每个渐变使用独立的坐标变量property --spot-1 { syntax: percentage; inherits: false; initial-value: -10%; } property --spot-2 { syntax: percentage; inherits: false; initial-value: 110%; } .neon__spot { background-image: radial-gradient( circle 9rem at var(--spot-1) 35%, rgba(255, 255, 255, 0.9) 0%, transparent 58% ), radial-gradient( circle 7rem at var(--spot-2) 65%, rgba(0, 240, 255, 0.7) 0%, transparent 55% ); animation: twin-sweep 8s ease-in-out infinite alternate; } keyframes twin-sweep { 0% { --spot-1: -10%; --spot-2: 110%; } 100% { --spot-1: 110%; --spot-2: -10%; } }两个变量一个从左侧进入、一个从右侧进入动画过程中刚好能在文字中段交错。视觉上就是两束“追光”擦肩而过。这里要克制一件事双追光的亮度不宜和单追光保持一致。两个光斑都很亮时文字中段会过曝字体会糊成一团。通常我会把第二个光斑的透明度再调低一点它更多承担“余光”的角色。5. 实测发现的坑光晕消失、锯齿、卡顿一次说清5.1 光晕消失不是浏览器bug是color透明连累text-shadow这个坑值得单独拿出来讲因为几乎所有第一次做聚光效果的人都会撞上。当你写出这样的代码时.neon__spot { color: transparent; text-shadow: 0 0 25px #0ff; background-clip: text; }你会发现光晕要么没有要么和背景图糊在一起。原因是text-shadow的颜色值如果省略会跟随color而这里color是transparent阴影也会变成“透明阴影”。即使显式写上颜色光照在透明文字轮廓上也会把背景图该显示的区域盖掉一层白雾。我的解决顺序是先确认底层不透明文字已经有正常光晕再独立加透明扫描层。如果为了减少 DOM 层非要在同一层里做那就老老实实把text-shadow的颜色全部显式写成 rgba并且做好接受“光斑对比度被削弱”的心理准备。分层才是长期可维护的路线。5.2 卡顿的根源往往不是CSS变量而是动错了层很多同学以为卡顿是因为 CSS 变量或 JS 事件太频繁。实测下来最常见的性能杀手是你把动画加在了text-shadow上。text-shadow每帧都会触发大量的模糊计算如果同时给多个方向的阴影做动画CPU 压力会直线上升。我的经验是不要动画化text-shadow本身而是让光晕层保持相对静态只让它接受背景图的扫描变化。background-image里的 radial-gradient 虽然也要参与图形合成但对比逐帧重算多层膨胀阴影开销要小得多。还有两个容易被忽视的卡顿来源。一是给元素加了过大的radial-gradient半径数据上明明是 30rem实际显示区域只有 300px浪费了 GPU 计算。二是鼠标事件回调里频繁操作getBoundingClientRect它会导致强制同步布局最好把坐标计算出的结果一次性写入变量不要在回调里做多余查询。5.3 弱浏览器和移动端的降级打法background-clip: text在主流现代浏览器里已经很稳但property对于 CSS 变量动画的支持范围仍然有限。旧内核浏览器遇到property不识别时整个关键帧动画会被跳过但变量仍然存在光斑会停在initial-value。所以降级策略很重要。我的标准降级方案是三层第一层是纯静态霓虹也就是基础文字加光晕这个在几乎所有浏览器里都能跑。第二层是扫描光斑的静态版本用一个不带动画的光斑放在文字左偏下的位置代替完全消失的扫描效果。第三层才是完整动态效果用supports或特性检测做增量加载。虽然property没有直接的supports查询但我常用一个兼容技巧先给默认的光斑位置设置一个视觉上能接受的中间值比如--spot-x: 20%然后动画只在支持的浏览器里覆盖。移动端还需要注意光斑半径。手机上元素宽度小10rem这种固定半径可能把整个小屏幕都照亮导致看不到文字轮廓。这时用clamp(4rem, 12vw, 10rem)做动态半径更靠谱。5.4 排查顺序和个人调试习惯遇到效果不对不要急着改代码先按下面顺序排查打开开发者工具找到扫描层.neon__spot看看background-image有没有被层叠覆盖background-clip是否真的生效。把--spot-x和--spot-y在开发者工具里手动改成50%看光斑是否跳到中间。如果能跳说明变量传递正常如果不动问题在 CSS 变量或层叠关系。确认color: transparent没有被其他规则覆盖否则背景图露不出来。关掉动画确认静态结构好看。静态不好看动画只是在放大问题。我自己调试时最常用的技巧是给扫描层临时加一圈outline: 2px solid red观察实际元素的边界范围。很多时候光斑没显示不是渐变错了而是元素高度塌陷、或者被文字层盖住。加轮廓线后所有空间问题一眼就能看到。6. 落地实战从Demo到能放进项目首屏6.1 适合放霓虹聚光灯的四个场景这个特效很炫但不是所有页面都适合。我的经验是这四个场景最容易出彩。个人主页首屏标题。暗黑风个人站配一个大字号霓虹标题鼠标一移光斑跟着亮整体质感立刻不一样。404 页面。这个场景很多人忽略了。404 页面本身没有太多信息用户停留时间短一个会动的霓虹大字能瞬间缓解“找不到页面”的尴尬还会让访客觉得这站很用心。配合一句提示文案和返回首页按钮体验很完整。直播开播封面或活动主视觉。这类内容通常要抓眼球霓虹聚光灯天然适合充当视觉中心。品牌折页式的 Banner。暗色背景加霓虹标题再加上双追光交叉扫过可以做活动开场或倒计时前的过渡动画。不太适合的场景是正文型页面、大量小字号标签、以及浅色背景页面。光效需要暗底衬托白底上做霓虹等于让演员在太阳底下打追光毫无意义。6.2 可直接抄的配置参数清单我把一套实测下来效果稳定的参数整理出来方便直接抄作业。项目推荐参数说明背景色hsl(240 30% 6%)深蓝黑比纯黑更有氛围字体字重 700 以上字距 0.04em笔画粗才能容纳光斑细节字号clamp(3rem, 10vw, 7rem)过小会糊光晕主色青色#00e5ff、玫红#ff00ff色相差拉开互补感强光斑半径clamp(5rem, 14vw, 11rem)与字号联动扫描周期6 到 10 秒太快伤眼太慢发闷动画曲线ease-in-out有加速减速才像追光文本层透明度底层不透明上层transparent两层职责分开这套参数用在一个 400px 宽的容器、一行大标题上是我最常用的配置。如果你放大到全屏 Hero只需要把字号和光斑半径一起放大比例关系保持不变。6.3 与故障风、粒子感组合的扩展想法霓虹聚光灯本身已经完整但它也很容易和其他 CSS 效果组合做出更复杂的视觉。我最常用的组合是加一点点故障风。给底层文字加两个伪元素一个用洋红色、一个用青色错位几个像素然后定期用关键帧切换transform: translate和clip-path制造信号撕裂的感觉。注意伪元素所在的层不要和扫描层混在一起否则画面会乱。故障风出现频率要低比如每 4 到 6 秒闪一次每次只持续 0.15 秒这是“信号不好”的克制表达。另一个方向是在背景上加一层微弱的噪点纹理用 CSS 生成一层低透明度的渐变或者 SVG Filter 都行。噪点能让霓虹光晕更接近荧光粉的颗粒感视觉上比纯渐变更耐看。最后如果项目里已经引入了 Canvas 粒子也可以让粒子只在聚光斑扫过时点亮其他时间保持暗色。这种联动复杂度高一些但效果是真的让人印象深刻。我自己的体会是任何炫技效果都必须先保证静态时也好看动态只是加分项。排查问题前先回到静态结构动画再炫也不能掩盖布局和配色的基础问题。把这套分层思路吃透你完全可以照着需求改出属于自己的“霓虹聚光灯”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑