如何实现滚动时在粘性容器内逐个触发动画标题

本文介绍一种可靠方案,通过监听滚动并结合 `getboundingclientrect()` 精确判断标题在粘性区域内的可视状态,实现在 `.sticky` 容器固定后仍能持续响应滚动、逐个激活标题动画的效果。

在构建滚动驱动的标题动画时,一个常见误区是直接依赖视口中心(如 viewportHeight / 2)触发状态切换——这在粘性定位(position: sticky)生效后极易失效,因为粘性元素脱离正常文档流后,其子元素的 getBoundingClientRect() 仍以视口为参考,但逻辑上需以粘性容器自身边界为判定基准。

正确的解法是:将滚动监听与粘性容器的局部坐标系对齐。核心思路是——仅当某个标题完全位于 .sticky 元素的可视区域内时,才将其设为 .active,其余标题统一移除该类。这样既避免了因粘性定位导致的“滚动冻结”假象,又确保动画严格按容器内顺序推进。

以下是完整可运行的实现:

const headings = document.querySelectorAll('.animated-text');
const sticky = document.querySelector('.sticky');

// 防抖优化:避免高频 scroll 触发重绘
let scrollTimer;
window.addEventListener('scroll', () => {
  clearTimeout(scrollTimer);
  scrollTimer = setTimeout(() => {
    const stickyRect = sticky.getBoundingClientRect();

    // 遍历每个标题,检查其是否完全处于 sticky 区域内
    headings.forEach((heading, index) => {
      const headingRect = heading.getBoundingClientRect();

      // 判定条件:标题顶部 ≥ sticky 顶部,且底部 ≤ sticky 底部
      const isInStickyView = 
        headingRect.top >= stickyRect.top && 
        headingRect.bottom <= stickyRect.bottom;

      if (isInStickyView) {
        // 激活当前项,关闭其他项
        headings.forEach((h, i) => {
          h.classList.toggle('active', i === index);
        });
      }
    });
  }, 16); // ~60fps 节流
});

对应的关键 CSS 需注意两点:

  • 使用 max-height 替代 height 实现高度过渡(height: auto 不支持 CSS 动画);
  • 移除可能干扰布局的 position: absolute,保持标题自然流式排列,确保 getBoundingClientRect() 获取真实位置。
.animated-text {
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  transition: opaci

ty 0.8s cubic-bezier(0.34, 1.56, 0.64, 1), max-height 0.8s ease; margin: 0; } .animated-text.active { opacity: 1; max-height: 1000px; /* 足够容纳任意内容的高度 */ }

⚠️ 注意事项

  • 确保 .sticky 元素有明确的 top 值(如 top: 0),否则 position: sticky 不生效;
  • 若标题内容高度差异大,建议统一设置 line-height 和 margin,避免因高度突变引发视觉跳动;
  • 在移动端需额外监听 touchmove(配合 passive: false)以兼容 iOS Safari;
  • 如需更精细控制(如部分进入即触发),可将判定逻辑改为 headingRect.top = stickyRect.top。

此方案不依赖第三方库,兼容主流浏览器,且保持 HTML 结构语义清晰——所有标题保留在同一 .text-animations 块级容器中,无需绝对定位或 JavaScript 操控 DOM 位置,真正做到了「结构不变、行为可控、动画流畅」。