波纹效果要想正常呈现,父容器必须设置 position: relative。道理其实很简单:我们需要一个定位基准点,才能将动态生成的波纹圆点精准放置在点击位置。如果父容器采用默认的 static 定位,新插入的绝对定位元素就会失去参照,直接飞向页面左上角,导致效果失效。
这里有一个容易被忽略的细节:并非只为最外层的包装盒设定相对定位就万事大吉。每一个触发波纹响应的按钮或区块,都必须单独声明 position: relative。尤其在使用 Bootstrap 这类 CSS 框架时,需要格外留意——.btn 等类名很可能已经预设了定位样式。如果受到框架默认样式的干扰,最稳妥的方式是显式覆盖,例如 .btn { position: relative !important; },确保定位生效。
用 getBoundingClientRect() 精确定位圆心
计算波纹圆心的坐标,是决定效果是否“跟手”的关键步骤。许多新手习惯直接使用 event.clientX / clientY 来计算坐标,这在页面未滚动时或许可行,但一旦出现滚动条,坐标就会产生偏移,导致波纹起点错位。
标准做法是调用 element.getBoundingClientRect()。该方法能够获取元素在视口(viewport)内的实时位置,精度达到小数位。接着,用点击时的 clientX 减去 rect.left,clientY 减去 rect.top,即可得到以元素自身为参照系的精确坐标。
这里有一个小细节:rect 返回的是浮点数,而 CSS 的 left 和 top 完全支持小数定位。因此,直接赋值即可,无需画蛇添足地使用 Math.round() 取整,避免造成像素级别的抖动,影响效果。
动画性能:用 transform: scale() 而非 width/height
波纹动画本质是缩放,但实现方式不同,性能差异巨大。如果为了方便直接修改元素的 width 和 height,浏览器就需要重新计算布局(Layout),频繁触发重排,帧率自然显著下降,尤其在性能一般的低端安卓机上,卡顿会非常明显。
正确且高效的做法是使用 transform: scale()。它只触发浏览器的合成(Composite)阶段,完全绕开 Layout 和 Paint,轻松跑满 60fps。只需在动画元素初始样式中设置 transform: scale(0),并配合 transform-origin: center,就能确保波纹从中心点开始缩放。
动画完成后务必进行清理。需要使用 setTimeout 将动态插入的波纹 DOM 节点从文档流中移除,否则用户多次点击后,内存中会积累大量无用元素,同样引发性能问题。常见设置为 setTimeout(() => ripple.remove(), 500),这里的 500 毫秒应与 CSS 动画的时长保持一致。关于动画时长,除了简单的 ease-out,更推荐使用 cubic-bezier(0.2, 0.8, 0.2, 1) 这类贝塞尔曲线,能让波纹的扩散与消失过程更加自然、富有质感。
事件处理:绕过移动端的300ms延迟
在 iOS 及部分老版本安卓浏览器上,click 事件存在 300 毫秒的延迟。这意味着用户用手指点击后,需要等待片刻波纹才会响应,产生“慢半拍”的迟滞感,严重影响交互体验。
解决方案是舍弃 click,改用 touchstart。不过为了同时兼容鼠标设备,最优雅的做法是统一监听 pointerdown 事件。该事件是 W3C 标准,能够统一处理鼠标、触摸和触控笔的交互行为。
绑定事件时,有一个重要的选项:{ passive: false }。在 iOS Safari 中,如果不显式设为 false,浏览器可能认为你不需要调用 preventDefault() 来阻止默认行为,从而忽略该事件的某些特性。虽然实现波纹效果不一定需要阻止默认行为,但加上 passive: false 是一个稳妥的习惯,可以确保事件被完整处理。仅用一个 pointerdown 事件即可,无需同时监听 click 和 touchstart,否则会导致事件重复触发,引发不可预知的问题。
一个真正好用的波纹效果,实际表现好不好,卡在哪里?无非三个关键点:定位是否随滚动实时校准、缩放动画是否通过 GPU 合成通道、事件是否绕过了 300 毫秒延迟。这三环环环相扣,任何一环缺失,波纹要么“飘了”、要么“卡了”、要么“慢半拍”,最终效果都会让用户感觉不对劲。
