单层::after做不出真实波纹感,因其仅能实现匀速放大后突变消失的机械动画;真实波纹需多层同心圆按0.1s–0.2s递增延迟依次膨胀,配合radial-gradient光晕、统一border-radius:50%及1.2s–1.6s长动画时长,才能模拟水波层层推进的纵深节奏。

为什么单层 ::after 做不出真实波纹感
单个圆形伪元素加 scale 动画,只会产生“匀速放大→突然消失”的机械感,缺乏水波由中心向外层层推进的纵深节奏。真实波纹是多个同心圆在不同时间点依次膨胀、透明度渐变、尺寸微调形成的叠加效果,必须靠多层独立动画协同实现。
- 每层
::before/::after需单独定义animation-delay,延迟值建议按 0.1s–0.2s 递增(如0s、0.15s、0.3s) - 各层
border-radius必须统一为50%,否则边缘会畸变 - 动画时长(
animation-duration)建议设为 1.2s–1.6s,太短显得急促,太长拖沓
三层伪元素的具体写法与关键参数
用 ::before、::after 加一个额外的包裹元素(如 .ripple 内再套 <span></span>),凑足三层最稳妥——伪元素本身不支持嵌套伪元素,所以第三层得靠真实子元素或 JS 插入。
.loader {
position: relative;
width: 40px;
height: 40px;
}
.loader::before,
.loader::after,
.loader .ripple {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,0,0,0.2) 0%, transparent 70%);
animation: ripple 1.4s infinite;
}
.loader::before { animation-delay: 0s; }
.loader::after { animation-delay: 0.2s; }
.loader .ripple { animation-delay: 0.4s; }
@keyframes ripple {
0% { transform: scale(0.3); opacity: 1; }
100% { transform: scale(2.5); opacity: 0; }
}
-
radial-gradient比纯background-color更接近真实波纹的衰减光晕 -
transform: scale()必须搭配transform-origin: center(默认就是 center,可不写) - 别漏掉
position: absolute和top: 0; left: 0,否则伪元素会脱离定位上下文
Chrome/Firefox 中动画卡顿的常见原因
多层 transform + opacity 在旧版浏览器里容易触发重排(reflow),导致掉帧。核心是避免让动画属性触发 layout 计算。
- 确保所有波纹层都加了
will-change: transform, opacity(仅在动画开始前加,结束后移除更佳,但简单场景可常驻) - 禁用
box-shadow或filter等高开销属性,它们会让 GPU 渲染压力陡增 - 如果父容器有
overflow: hidden,检查波纹是否被意外裁剪——scale(2.5)后直径达原始 2.5 倍,需预留足够空间或改用clip-path
移动端点击反馈中波纹定位偏移怎么办
在按钮上做点击波纹时,::after 经常以按钮左上角为原点扩散,而不是点击位置。这是因为伪元素默认锚点是父元素左上角,不是事件坐标。
- 必须用 JS 获取
touchstart/mousedown的clientX/clientY,动态设置left和top(注意减去父元素getBoundingClientRect()偏移) - 纯 CSS 无法响应点击坐标,所谓“CSS 波纹”只适用于居中加载状态,不适用于 Material Design 风格的交互反馈
- 若坚持用 CSS,只能妥协:把波纹容器设为
position: relative,并确保按钮宽高固定、无 padding 影响定位基准
animationend 事件手动移除临时元素,或者用 CSS animation-iteration-count: 1 配合 JS 控制触发时机。前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











