用 Intersection Observer 打造丝滑的级联滚动动画
🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
无需任何动画库,仅用原生 Web API 实现滚动时丝滑的淡入滑入效果,兼顾性能与体验。
你是否见过这样的交互动效:
- 用户滚动页面时,一组卡片像被“唤醒”一样,依次从下方滑入并淡入;

- 如果这些元素在页面加载时已在视口内,它们也会自动按顺序浮现。

这种效果不仅视觉流畅,还能有效引导用户注意力,提升内容层次感。更重要的是——它不依赖 GSAP、AOS 等第三方库,仅靠 Intersection Observer + CSS 动画 + 少量 JavaScript,就能实现高性能、可访问、且高度可控的滚动触发型级联动画。
今天,我们就来一步步拆解这个经典动效,并给出一套可直接复用的轻量级方案。
🔧 核心原理概览
整个动画系统依赖三个关键技术点:

最关键的设计哲学是:动画只在用户能看到它的时候才执行,既节省性能,又避免“闪现”。
🧱 HTML 结构(简化版)
为便于理解,我们剥离业务逻辑,只保留动效核心:
<div class="container">
<ul class="card-list">
<li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 1;"
>Card 1</li
>
<li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 2;"
>Card 2</li
>
<li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 3;"
>Card 3</li
>
<!-- 更多卡片... -->
</ul>
</div>
💡 类名与属性说明
.scroll-trigger:表示该元素需要被滚动监听;.animate--slide-in:启用滑入动画;data-cascade:JS 识别“需设置动画顺序”的标志;--animation-order:CSS 自定义属性,用于计算延迟时间(如第 2 个元素延迟 150ms)。
🎨 CSS 动画定义
:root {
--duration-extra-long: 600ms;
--ease-out-slow: cubic-bezier(0, 0, 0.3, 1);
}
/* 仅在用户未开启“减少运动”时启用动画(晕动症用户友好) */
@media (prefers-reduced-motion: no-preference) {
.scroll-trigger:not(.scroll-trigger--offscreen).animate--slide-in {
animation: slideIn var(--duration-extra-long) var(--ease-out-slow) forwards;
animation-delay: calc(var(--animation-order) * 75ms);
}
@keyframes slideIn {
from {
transform: translateY(2rem);
opacity: 0.01;
}
to {
transform: translateY(0);
opacity: 1;
}
}
}
✨ 参数说明

✅ 无障碍提示:通过
@media (prefers-reduced-motion)尊重用户偏好,对晕动症用户更友好。
🕵️ JavaScript:Intersection Observer 监听逻辑
为什么不用 scroll 事件?
传统方式:
// ❌ 性能差,频繁触发
window.addEventListener('scroll', checkVisibility);
现代方案:
// ✅ 高性能,浏览器底层优化 const observer = new IntersectionObserver(callback, options);
完整监听逻辑
const SCROLL_ANIMATION_TRIGGER_CLASSNAME = 'scroll-trigger';
const SCROLL_ANIMATION_OFFSCREEN_CLASSNAME = 'scroll-trigger--offscreen';
function onIntersection(entries, observer) {
entries.forEach((entry, index) => {
const el = entry.target;
if (entry.isIntersecting) {
// 进入视口:移除 offscreen 类,允许动画播放
el.classList.remove(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
// 若为级联元素,动态设置顺序(兜底)
if (el.hasAttribute('data-cascade')) {
el.style.setProperty('--animation-order', index + 1);
}
// 只触发一次,停止监听
observer.unobserve(el);
} else {
// 离开视口:加上 offscreen 类,禁用动画
el.classList.add(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
}
});
}
function initScrollAnimations(root = document) {
const triggers = root.querySelectorAll(`.${SCROLL_ANIMATION_TRIGGER_CLASSNAME}`);
if (!triggers.length) return;
const observer = new IntersectionObserver(onIntersection, {
rootMargin: '0px 0px -50px 0px', // 元素进入视口 50px 后才触发
threshold: [0, 0.25, 0.5, 0.75, 1.0],
});
triggers.forEach((el) => observer.observe(el));
}
// 页面加载完成后启动
document.addEventListener('DOMContentLoaded', () => {
initScrollAnimations();
});
🎯 关键设计细节
rootMargin: '0px 0px -50px 0px':确保元素完全进入用户视野后再触发动画,避免“刚看到就结束”;- 初始所有
.scroll-trigger元素默认带有.scroll-trigger--offscreen类,阻止 CSS 动画生效; unobserve:动画只播放一次,避免重复触发,节省资源。
📊 两种场景下的行为对比

这正是你感受到的“丝滑感”来源:无论用户如何进入页面,动画总是在最合适的时机出现。
💡 总结:这套方案的优势

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。


浙公网安备 33010602011771号