最常用轻量方案是用@keyframes同时控制scale缩放和opacity透明度变化,配合transform-origin: center居中缩放,避免width/height重排,确保GPU加速。
animation + @keyframes 实现基础脉冲圆环纯 CSS 脉冲动画最常用、最轻量的方案就是靠缩放 + 透明度变化。关键不是“画一个波纹”,而是“模拟向外扩散的视觉错觉”。@keyframes 定义从中心点放大并淡出的过程,再用 animation 应用到一个圆形元素上即可。
常见错误是直接给 border-radius: 50% 的 div 加 transform: scale(),但没设 transform-origin: center,导致缩放中心偏移;或者忘了加 opacity 过渡,看起来像硬切而不是柔和脉冲。
实操建议:
position: relative,脉冲层设 position: absolute 并居中(top: 50%; left: 50%; transform: translate(-50%, -50%))transform: scale() 和 opacity,例如:@keyframes pulse { 0% { transform: scale(0.8); opacity: 0.7; } 70% { transform: scale(1.3); opacity: 0.3; } 100% { transform: scale(1.5); opacity: 0; }}
width/height 动画——重排开销大,transform 和 opacity 才能触发 GPU 加速span 元素单个脉冲容易,但点击触发多层扩散(比如按钮点击后弹出 3 圈渐次延时的波纹),CSS 就不够用了。这时候得靠 JS 在事件触发时动态插入带不同 animation-delay 的元素。
立即学习“前端免费学习笔记(深入)”;
典型场景:按钮点击反馈、地图标记点击高亮。很多人试图用伪元素(::after)做多层,结果发现无法单独控制每层延迟或无法复用——伪元素只能有一个,且不能动态增删。
实操建议:
span,设置 class 如 ripple,并立即 append 到目标元素内.ripple 设统一动画,但通过 JS 设置 style.animationDelay = `${i * 0.2}s` 控制每圈节奏element.remove(),否则 DOM 节点持续堆积(尤其高频点击时)pointer-events: none 到波纹元素上,避免遮挡后续点击:active 和 touch-action
在 iOS 或部分安卓 WebView 里,点击后脉冲不触发,大概率不是动画写错了,而是浏览器把点击事件吞掉了,或者触发了默认的“高亮蒙层”干扰了自定义效果。
常见表现:click 事件能打 log,但波纹没出现;或者第一次点有效,之后就失效。这往往和 :active 状态样式冲突,或 touch-action: manipulation 没配对有关。
实操建议:
style.touchAction = 'manipulation'(或 CSS 中写 touch-action: manipulation),允许浏览器优化触摸行为,同时不阻断 JS 事件:active 里写 opacity: 0.5 这类样式——它会和脉冲动画的 opacity 冲突,造成闪烁或中断webkit-tap-highlight-color: transparent 缺失(iOS 默认有灰色点击高亮,会盖住你的波纹)box-shadow 和 filter
加了阴影或模糊滤镜的脉冲看起来更“真实”,但代价巨大:每帧都要重绘整个图层,GPU 压力飙升,尤其在低端安卓机上直接卡成 PPT。很多开发者直到上线才意识到问题。
错误做法:用 filter: blur(4px) 模拟波纹边缘柔化;或给每个波纹层加 box-shadow: 0 0 12px rgba(0,0,0,0.2)。
实操建议:
background: radial-gradient() 配合透明度过渡,比 blur() 性能好 3 倍以上box-shadow,只加在最后一帧(100% keyframe),且 blur 半径 ≤ 6px真正难的不是写出第一个脉冲,而是让 5 个波纹在千元机上连续触发还不掉帧,同时不破坏可访问性(比如屏幕阅读器仍能正常播报按钮状态)。这些细节藏在 animation-timing-function 的选择、will-change 的克制使用、以及是否监听 animationend 而非 setTimeout 里。