不能用 :hover 实现真波纹,因它无法获取点击坐标、不响应真实交互事件、移动端失效且不可访问;必须用 mousedown/touchstart 获取 clientX/clientY 并动态创建像素定位的 ripple 元素。

Hover 时触发波纹效果是反模式,:hover 无法获取点击坐标,也违背 Material Design 规范——波纹必须响应**真实交互事件(mousedown/touchstart)**,不是悬停。强行用 :hover 模拟只会得到居中扩散、移动端失效、键盘不可访问的假效果。
为什么不能用 :hover 实现真波纹
波纹的核心要求是“点哪炸哪”,而 :hover 是无状态伪类:它不接收鼠标位置参数,无法调用 getBoundingClientRect(),也不能监听 clientX/clientY。所有基于 :hover::after 的方案都只能固定锚点(通常是 top: 50%; left: 50%),结果就是无论你把鼠标移到按钮哪个角落,波纹永远从正中心炸开——这既不符合用户预期,也不满足 WCAG 可访问性要求。
更严重的是,iOS Safari 和部分安卓浏览器对 :hover 支持极差:悬停可能延迟、不触发,甚至在触摸设备上完全无效。而 mousedown 或 touchstart 才是真正可靠的交互起点。
正确做法:用 mousedown + touchstart 替代 hover
Bootstrap 按钮默认已设 position: relative,只需绑定原生事件,不依赖任何框架:
- 监听
mousedown(桌面)和touchstart(移动),用event.touches?.[0] || event统一取坐标 - 计算相对偏移:
const rect = btn.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; - 动态创建
<span class="ripple"></span>,设style.left = x + 'px'、style.top = y + 'px'、style.width和style.height为直径(建议Math.max(btn.clientWidth, btn.clientHeight) * 2) - 动画结束必须监听
animationend,而非setTimeout;否则快速连点会堆积 DOM 节点
常见踩坑点:overflow 和定位上下文
波纹元素会溢出按钮边界,除非你明确控制裁剪和定位:
- 按钮自身必须有
overflow: hidden—— 不是父容器,也不是body;否则波纹放大后直接飞出视口 - 按钮必须保持
position: relative;若父级有transform(如scale(0.95)或translateZ(0)),getBoundingClientRect()返回值会失真,此时需把波纹挂到该父级下,并重新计算坐标 - 别用
%设置left/top(如left: 50%),这会让波纹永远居中;必须用像素值锚定点击点 -
.ripple元素必须含pointer-events: none,否则会拦截后续点击
disabled 状态必须彻底禁用波纹
按钮禁用时,波纹不仅视觉上要消失,逻辑上更要阻止事件绑定:
- 初始化时检查
btn.hasAttribute('disabled') || btn.classList.contains('disabled') - 在事件监听器开头加守卫:
if (btn.disabled || btn.matches('[disabled]')) return; - 键盘用户按空格键触发
mousedown时,也要同步判断;仅靠button[disabled]无法拦截所有情况
真正难的不是写动画,而是让每个波纹都精准落在毫秒级的点击坐标上,同时扛住连点、缩放、禁用、键盘焦点等真实场景——这些细节漏掉一个,效果就垮掉。


















