纯 CSS 的 :active 水波纹总从按钮中心炸开,因为 :active 无法读取点击坐标,圆心只能固定在 left: 50%、top: 50%;需 JavaScript 结合 getBoundingClientRect() 与 touches[0] 动态计算真实点击位置,并用 transform: translate(x, y) scale(s) 定位缩放,配合 will-change: transform 和 overflow: hidden 确保 Safari 兼容性。
:active 水波纹总从按钮中心炸开因为 CSS 无法读取点击坐标,:active 只能触发预设动画,圆心固定在 left: 50%、top: 50%。这适合图标按钮或尺寸统一的场景,但用户点左下角时波纹却从正中弹出,反馈失真。
常见错误是以为加了 border-radius: 50% 和 transform: scale() 就够了——漏掉 position: relative,伪元素会相对于 body 定位;没设 overflow: hidden,波纹放大后会撑出圆角边界。
position: relative,否则 ::after 找不到定位基准::after 必须含 content: ""、position: absolute、border-radius: 50%
transition: transform 0.6s, opacity 0.6s,禁用 transition: all
pointer-events: none 防止伪元素遮挡连续点击touchstart 和 touch-action
iOS Safari 和部分安卓 WebView 对 click 有 300ms 延迟,且 :active 在无焦点元素上根本不会激活。仅靠 cursor: pointer 没用,那是桌面端逻辑。
必须用 JavaScript 监听 touchstart,并给按钮显式设置 touch-action: manipulation。否则 Safari 会等待判断是否双指缩放,导致波纹延迟或不触发。
立即学习“前端免费学习笔记(深入)”;
touch-action: manipulation 要写在按钮自身上,父容器无效e.touches[0].clientX,不是 e.clientX
<div>,需加 tabindex="0" 获取焦点,否则 :active 不生效touchstart 后,立即调用 e.preventDefault()(仅当不需要滚动时)getBoundingClientRect() 算坐标时最容易错哪几处所有偏移问题都源于没把视口坐标转成相对坐标。浏览器里 e.clientX 是相对于整个窗口的,而伪元素的 left/top 是相对于最近的 position: relative 父元素——不转换就必然错位。
漏掉 const rect = button.getBoundingClientRect() 这一行,后面全白算。更隐蔽的坑是:嵌套了 transform 的容器里,e.offsetX 失真且 IE 不支持,必须坚持用 clientX - rect.left。
button.style.setProperty('--x', '0px'),否则新波纹继承上一次位置Math.max(0, Math.min(x, width - diameter)) 限制圆心不越界,避免波纹从按钮外侧“挤”进来e.touches?.[0] || e 兼容 touch/click 双事件top/left + scale() 组合有渲染 bug,强制改用 transform: translate(x, y) scale(s)
这些问题集中在高 DPR 屏幕和 Safari,跟缓动函数无关,是渲染管线层面的缺陷。不加 will-change: transform,Safari 就不触发硬件加速,首帧空白或卡顿;小数像素缩放引发 subpixel 插值模糊,导致边缘发虚。
最稳妥的组合是:transform: translate(-50%, -50%) scale(s) + will-change: transform + overflow: hidden。别信“加个 backface-visibility: hidden 就行”,它不如 will-change 有效。
will-change: transform,Safari 必须这个才走 GPU 渲染Math.round(s * 10) / 10 截断小数,比如 scale(2.345) → scale(2.3)
overflow: hidden,否则 Safari 会裁切动画轨迹,尤其圆角按钮真实项目里,最常被忽略的是 Safari 对 transform-origin 和 top/left 混用的兼容性断裂——它要求你彻底放弃 top/left 定位,只用 transform: translate(),哪怕多写两行 JS 计算也得换。