
本文讲解如何利用 intersection observer api 结合 css 自定义属性与条件动画类,精准触发多个标题的“逐字入场”动画,避免页面加载时全部同时播放。
本文讲解如何利用 intersection observer api 结合 css 自定义属性与条件动画类,精准触发多个标题的“逐字入场”动画,避免页面加载时全部同时播放。
在网页中实现“标题逐字动画”(word-by-word reveal)效果时,一个常见误区是:将动画样式直接绑定在基础元素上(如 .heading-reveal span),导致即使尚未进入视口,浏览器仍会解析并准备动画——一旦 --word-index 被设置,所有 <span></span> 立即触发延迟动画,造成“页面一加载就闪现执行”的问题。
根本原因在于:CSS 动画在 animation 属性被声明且元素满足可动画条件(如 opacity 从 0→1)时即开始计时;而你的原始代码中,--word-index 和 reveal 类是在 isIntersecting 为 true 时才统一添加的,但由于动画规则始终生效,所有 <span></span> 的 animation-delay 实际上都从 0s 开始计算(因 --word-index 初始未定义,默认为 0),最终表现为“瞬间集体播放”。
✅ 正确解法是:将动画声明完全隔离到 .reveal 类中,确保只有当元素真正进入视口、显式获得该类时,动画才被激活。
以下是优化后的完整实现:
✅ HTML 结构(保持语义清晰)
<h1 class="heading-reveal"> <span>Title</span> <span>Goes</span> <span>Here</span> </h1> <div class="spacer" style="height: 100vh;"></div> <h1 class="heading-reveal"> <span>2nd</span> <span>Title</span> <span>Here</span> </h1> <div class="spacer" style="height: 100vh;"></div> <h1 class="heading-reveal"> <span>3rd</span> <span>Title</span> <span>Here</span> </h1>
✅ CSS:分离状态与动效
.heading-reveal {
display: flex;
flex-wrap: wrap;
margin: 2rem 0;
}
.heading-reveal span {
opacity: 0; /* 初始完全隐藏 */
/* 不在此处声明 animation —— 避免预加载触发 */
}
/* ✅ 动画仅在 .reveal 存在时生效 */
.heading-reveal span.reveal {
animation: revealWord 0.5s forwards;
animation-delay: calc(var(--word-index, 0) * 0.25s); /* fallback to 0 if --word-index missing */
}
@keyframes revealWord {
to { opacity: 1; }
}✅ JavaScript:精准监听 + 按需注入
window.addEventListener('load', () => {
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 为当前标题内每个单词 span 设置索引并添加 reveal 类
entry.target.querySelectorAll('span').forEach((span, idx) => {
span.style.setProperty('--word-index', idx);
span.classList.add('reveal');
});
obs.unobserve(entry.target); // ✅ 一次性触发,避免重复回调
}
});
},
{
root: null, // 使用视口为根容器
rootMargin: '0px', // 可选:设为 '0px 0px -100px' 实现“稍早触发”
threshold: 0.1 // 元素 10% 进入视口即触发(已验证有效)
}
);
document.querySelectorAll('.heading-reveal').forEach(el => {
observer.observe(el);
});
});⚠️ 关键注意事项:
-
不要移除
--word-index的var(--word-index, 0)fallback:防止因 JS 执行顺序或异常导致未设置变量时calc()计算失败; -
unobserve()必须放在isIntersecting分支内:否则可能因滚动抖动反复触发; -
rootMargin进阶技巧:若希望标题在完全进入前就开始动画(如“提前半屏”),可设为'0px 0px -200px'(负 bottom margin 表示向上扩展检测区域); - 性能友好:Intersection Observer 原生异步,无重排重绘风险,适合多标题场景。
通过以上结构化分离(CSS 状态控制 + JS 时机控制),每个标题都能严格在其进入用户可视区域时,独立、准确地启动逐字动画——既提升用户体验,又保障性能与可维护性。

















