在一些数据展示或打卡类页面里,经常能看到一种“水球”效果:一个圆形容器里装着水,水面微微晃动,水位随着进度上涨,点击时还会泛起涟漪。这种效果看似需要 Canvas 或 SVG,其实只用 CSS 就能实现,核心思路是利用 border-radius 画出圆形容器,用 overflow: hidden 把超出边界的水面裁掉,再通过旋转和动画让水面看起来在波动。下面从圆形容器搭建、水面制作、波动动画到涟漪扩散,一步步拆解实现方案。

一、搭建圆形容器:画一个“玻璃杯”
第一步是画出圆形的外框。这一步非常简单,一个正方形的 div 加上 border-radius: 50% 即可。为了让它更像一个装水的容器,通常会加上边框、内阴影和淡淡的背景色,营造出玻璃球的质感。
.water-ball {
width: 200px;
height: 200px;
border-radius: 50%;
border: 4px solid #2a9d8f;
background: rgba(42, 157, 143, 0.1);
position: relative;
overflow: hidden; /* 关键:裁剪超出圆形的水面 */
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
}
这里有个细节要特别注意:overflow: hidden 是整个效果的灵魂。水面元素本质上是一个比容器大得多的矩形,靠这个属性把它裁剪成圆形。如果不加,水面会以方形的样子溢出,效果立刻穿帮。
另外,如果希望水位是可变的(比如表示 60% 的进度),建议用行内样式或者 CSS 变量控制水面的位置,方便 JS 动态更新。可以定义一个 --percent 变量,后面所有和水位的计算都围绕它展开。
二、制作会倾斜的水面
水面是整个效果里最巧妙的部分。做法是:创建一个很宽的矩形(宽度至少是容器的两倍),给它设置圆角让上边缘呈弧形,然后旋转一个小角度(比如 5 度),再定位到容器中。旋转之后,这个矩形看起来就像一个斜面,配合外层容器的圆形裁剪,视觉上就是“一杯斜着的水”。
<div class="water-ball"> <div class="water"></div> <div class="water water2"></div> </div>
.water {
position: absolute;
width: 400px; /* 比容器宽一倍,保证旋转后仍能覆盖 */
height: 400px;
border-radius: 40%; /* 上边缘形成弧线 */
background: rgba(42, 157, 143, 0.8);
left: 50%;
top: 60%; /* 水位:数值越小水越多 */
transform: translateX(-50%) rotate(0deg);
}
.water2 {
background: rgba(42, 157, 143, 0.5);
top: 62%; /* 略低于第一层,形成叠加层次 */
}
水位控制的关键在 top 属性。假设容器高 200px,想让水面停在 60% 高度处,就把 top 设为 60%(水面元素自身的坐标原点在水面上沿)。两层水叠加,一层深一层浅,会有明显的水体通透感。
这里要提醒一个常见误区:border-radius: 40% 和 50% 的区别很大。40% 会形成更扁的椭圆弧,转起来像水波;50% 接近正圆,旋转时上边缘几乎不产生起伏。做水波效果时选 35% 到 45% 之间通常观感最好。
三、让水面动起来:波动动画
静态的水没有生命力,接下来给两层水加上旋转动画。注意旋转的是水面元素本身,而不是容器。因为水面是带圆角的矩形,绕自身中心旋转时,上边缘的弧线会周期性地抬起和压下,看起来就是水在晃动。
@keyframes wave {
from { transform: translateX(-50%) rotate(0deg); }
to { transform: translateX(-50%) rotate(360deg); }
}
.water {
animation: wave 6s linear infinite;
}
.water2 {
animation: wave 9s linear infinite; /* 速度不同,叠加出自然的波形 */
}
两层水使用不同的动画时长(比如 6 秒和 9 秒),它们的波峰波谷会不断错开又重合,产生类似真实水面的干涉效果。如果两层速度一样,波形会完全同步,看起来像一块塑料片在转,非常假。
还可以在水面元素上加一点模糊或透明度渐变,让水更柔和。比如给第二层水设置 filter: blur(1px),或者用 linear-gradient 做背景,从上到下颜色逐渐加深,模拟水的深度感。这些小细节能显著提升真实度。
四、模拟点击涟漪扩散效果
水球效果里最出彩的是涟漪:点击后从触点处扩散出一圈一圈的水纹。纯 CSS 做涟漪有两种常见方案,一种是 Material Design 风格的缩放圆,一种是描边圆环扩散,这里用第二种,更贴近水纹的观感。
<div class="water-ball" id="ball"> <div class="water"></div> <div class="water water2"></div> </div>
const ball = document.getElementById('ball');
ball.addEventListener('click', function (e) {
// 计算点击点相对容器的坐标
const rect = ball.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 创建多个涟漪圈,错开延迟形成一圈圈扩散
for (let i = 0; i < 3; i++) {
const ripple = document.createElement('span');
ripple.className = 'ripple';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
ripple.style.animationDelay = (i * 0.25) + 's';
ball.appendChild(ripple);
// 动画结束后移除节点,避免堆积
ripple.addEventListener('animationend', () => ripple.remove());
}
});
.ripple {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.8);
transform: translate(-50%, -50%) scale(1);
animation: ripple-grow 1.2s ease-out forwards;
pointer-events: none;
}
@keyframes ripple-grow {
from {
transform: translate(-50%, -50%) scale(1);
opacity: 0.9;
}
to {
transform: translate(-50%, -50%) scale(18);
opacity: 0;
}
}
这个方案的原理是:点击时动态创建若干个空白的描边圆环,把它们定位到点击坐标上,然后播放放大加淡出的动画。三个涟漪通过 animation-delay 依次触发,看起来就是一圈接一圈往外扩散。由于外层容器有 overflow: hidden,涟漪扩散到圆形边缘时会被自动裁掉,效果非常自然,不需要额外计算裁剪路径。
要注意 pointer-events: none 这个属性,它防止涟漪元素挡住后续的点击事件。如果没有它,涟漪还飘在半空时再点一次,点击可能落在涟漪元素上导致坐标计算错乱。另外记得在动画结束后移除节点,否则点击几十次后 DOM 里会堆积大量残留元素,影响性能。
五、性能与兼容性建议
整套效果用到的属性包括 transform、border-radius、opacity 和 animation,全部是 GPU 加速友好的属性,即使页面上放多个水球也不会明显掉帧。需要避免的是用 top、left 做波动动画,那会触发重排,性能差距很大。上文代码里动画只改 transform,水位变化才改 top,这是有意为之的分工。
如果要做水位从 0% 涨到目标值的动画,可以配合 CSS 变量加一个补间:JS 每隔一小段时间更新 --percent,或者直接用 Web Animations API 驱动 top 的过渡。再加上水面上的百分比文字(用一个绝对定位的元素盖在水球上即可),一个完整的“水球进度”组件就成型了。把这套结构封装成 Web Component 或者 Vue、React 组件,就能在任意项目里复用。
CSS水波纹涟漪动画border-radius修改时间:2026-09-06 03:32:42