Blog

CSS only 的板块呼吸动画

前端CSS动画响应式

让页面有"活"的感觉

首页的 9 个板块是纯静态内容区域,如果不做任何处理,滚动过去就是一段段死文字。板块呼吸动画的目标:给当前所在的板块一个微妙的"活"的感觉——背景微微扩展,内容微微放大——告诉用户"你在这里"。

padding 而不是 scaleY

最先想到的是 transform: scaleY(1.1):

.section.snap-active {
  transform: scaleY(1.1);
  transition: transform 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
}

但测试时发现一个问题:两个相邻板块都是半透明暗色背景,scaleY 会在板块之间造成一小段重叠。重叠处的半透明叠加,产生一条明显的深色缝隙。

padding 扩展:

.section.snap-active {
  padding-top: 10.5rem;    / 默认 8rem → +40px /
  padding-bottom: 10.5rem;
  transition: padding 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.section.snap-active > div {
transform: scale(1.08);
transition: transform 0.55s cubic-bezier(0.34, 1.56, 0.64, 1);
}

padding 扩展不会产生视觉重叠,因为它实际改变了元素盒子的大小——相邻板块被推开了,而不是叠加。同时内部内容 scale(1.08) 让文字/按钮跟着放大,形成内外一致的呼吸感。

只在桌面端生效

@media (pointer: fine) {
  .snap-active { / 呼吸动画 / }
}

pointer: fine 大致等价于"有鼠标的设备"。手机上触摸屏是 pointer: coarse,整个呼吸动画规则被略过。因为:
1. 手机端没有 SnapNav 吸附(自由滚动),不存在"当前板块"的概念
2. 手机屏幕小,板块内容需要更多空间,背景扩展会挤掉内容区域
3. 手机 CPU 弱,省一个动画省一点电

snap-active 的切换

哪个板块是"当前"的?不是靠 Intersection Observer(虽然很多场景它是最佳选择),而是靠 SnapNav 的 onScroll 中实时计算 findClosest()。每次滚动事件触发,算出视口中心最近的板块,对比上一个是否变化,变化就切换 className。

用滚动事件而非 Observer 的原因:需要和 SnapNav 的吸附逻辑共用同一套"当前板块"判断。两个系统用两套判定逻辑会出矛盾——吸附说"我在板块 3",呼吸动画说"板块 4 更近",用户看到的就是错乱。

为什么不用 JavaScript 动画

这个动画完全可以用 requestAnimationFrame 的 lerp 来做,和网站其他动效保持统一。但呼吸动画恰好是那种不需要帧级精确的场景——它不跟随鼠标,不响应拖拽,只是"当前板块变大一丁点"。CSS transition 完全够用,还不用写 JS 代码。少写一行代码比统一技术栈更重要。