深蓝色的水面上,一圈圈白色的波纹从中心向外扩散,越往外越淡、越平缓,整体带着透视被压扁成椭圆,看上去就像俯视一潭静水被滴了一滴水。右上角有一枚水滴形状的按钮,点开是一块玻璃质感的面板,可以开关模糊、调节同时存在的波纹圈数(3 到 5 圈)和扩散速度(1 到 4 倍),改动即时生效。
它没有 JavaScript,也没有用 Canvas 或 SVG。整个水面由一百个空的 div 叠成,每个是一圈椭圆描边;所有圈用的是同一条只上下移动几像素的动画——真正制造“向外扩散”错觉的,是每一圈各不相同的动画延迟。
这套水波特效的四个核心机制
一百个椭圆,一条动画。每个圈只做一件事:在自身位置上下轻微摆动。摆动幅度按序号递减,外圈几乎不动,于是波峰在视觉上是“传递”过去的,而不是每个圈各自缩放。
扩散感来自负的动画延迟。延迟按序号线性递增(取负值让动画从中途开始),相邻两圈的相位差固定,这条相位梯度就是波的传播方向——改一个变量,圈数就从 3 变成 5,波长随之改变。
尺寸用容器查询单位。圈的宽度按容器宽度的百分比计算,配合固定的宽高比压成椭圆,所以整片水面随容器缩放,在任何屏幕上比例都一致。
控制面板是纯 CSS 状态机。页面顶部藏着一组复选框和单选框,面板上的每个加减按钮其实是指向下一档的标签,选中态通过兄弟选择器把变量值改掉,同时用伪元素显示当前档位。
三个把“假水波”做真的关键
其一,振幅必须随半径衰减。摆动幅度写成随序号递减的形式,最内圈幅度最大、最外圈趋近于零。真实水波的能量随传播距离扩散而衰减,若所有圈幅度一致,画面会像一叠整齐晃动的呼啦圈,一眼假。透明度也按同一思路递减,双重衰减叠加后才有“远处渐渐平息”的感觉。
其二,延迟公式同时受圈数和速度控制。核心是这样一条计算:
animation-delay: calc(-2 * var(--speed) * var(--ripples) + ...);
前半段是整体偏移,保证页面一打开波纹就已经在中间状态而不是从静止开始;后半段按序号铺开相位。因为圈数和速度都以变量形式参与计算,切换档位时不需要重写任何关键帧,浏览器会重新计算每一圈的延迟。这正是“CSS 状态机”能真正驱动动画的原因。
其三,模糊是可选而非必需。开启后给整片场景加一点点模糊,把一百条 1 像素描边糊成连续的水面,质感立刻上一个台阶;关掉则能清楚看到每一圈的构成,方便理解原理。做成可切换而不是写死,对学习和二次开发都友好。
拿到源码后能改什么
最直接的是三个变量:圈的总数、同时可见的波纹圈数、动画周期。总数决定精细度,一百圈已经相当细腻,若要放在小尺寸区域,减到五六十圈也看不出差别,还能省下不少渲染开销。
形状由宽高比控制,当前是 3,也就是宽是高的三倍,视觉上相当于俯角较低的视角。想做成正俯视就把它改成 1,圈会变成正圆;想让视角更平就加大这个值。
配色在页面底层的那道线性渐变上,从黑到亮蓝。改成暖色就是夕阳水面,改成青绿就是浅滩。描边颜色目前是白色,做彩色波纹时注意同时降低不透明度的基准值,否则外圈会显脏。控制面板的玻璃质感来自内阴影加圆角加边框的组合,那一块可以整体拿去复用在别的地方。
运行提示:效果依赖容器查询单位与 CSS 变量参与动画计算,需要较新版本的浏览器;旧版本上圈会失去尺寸梯度。整页无脚本,可直接双击 index.html 打开。若在低性能设备上觉得吃力,减少圈的总数是最有效的优化。建议使用近两年版本的 Chrome、Edge、Firefox 或 Safari。详细运行方法请参见压缩包内的帮助文档。
关于纯 CSS 水波的常见问题
能不能做成点击水面才泛起涟漪?
可以,但需要一点脚本来记录点击位置。一个不写脚本的折中做法是:用多个复选框对应几个预设位置,点击时切换整片水面的定位——适合固定几个触发点的场景。
放在页面里当背景会不会太重?
一百个元素各带一条动画,属于合成层内的位移,开销可控,但元素数量本身会增加样式计算。作为背景使用时建议把总数降到 50 上下,并关掉模糊。
手机上表现如何?
尺寸用的是容器相对单位,会自动适配;控制面板用的是原生表单控件,触摸同样可用。需要注意的是模糊在部分移动端 GPU 上开销较大,弱机建议默认关闭。
为什么我改了圈数,波纹却不动了?
检查改的是不是同时可见圈数那个变量,而不是元素总数。两者含义不同:总数是画了多少条线,圈数是这些线上分布几个完整波长,后者才是控制波纹密度的那个。
它适合放在什么地方
安静、循环、不抢眼,很适合做水务与环保企业的首页背景、冥想与助眠类应用的加载页、饮品品牌的产品页衬底,也可以作为加载动画或空状态插画使用。相位梯度、振幅衰减与纯 CSS 控制面板的完整写法都在源码里,结构简单易读,下载后可查看。