用 Intersection Observer 打造丝滑的级联滚动动画
用户滚动页面时一组卡片像被“唤醒”一样依次从下方滑入并淡入如果这些元素在页面加载时已在视口内它们也会自动按顺序浮现。这种效果不仅视觉流畅还能有效引导用户注意力提升内容层次感。更重要的是——它不依赖 GSAP、AOS 等第三方库仅靠Intersection ObserverCSS 动画 少量 JavaScript就能实现高性能、可访问、且高度可控的滚动触发型级联动画。今天我们就来一步步拆解这个经典动效并给出一套可直接复用的轻量级方案。 核心原理概览整个动画系统依赖三个关键技术点最关键的设计哲学是动画只在用户能看到它的时候才执行既节省性能又避免“闪现”。 HTML 结构简化版为便于理解我们剥离业务逻辑只保留动效核心1234567891011121314divclasscontainerulclasscard-listliclasscard scroll-trigger animate--slide-indata-cascade style--animation-order: 1;Card 1/liliclasscard scroll-trigger animate--slide-indata-cascade style--animation-order: 2;Card 2/liliclasscard scroll-trigger animate--slide-indata-cascade style--animation-order: 3;Card 3/li!-- 更多卡片... --/ul/div 类名与属性说明.scroll-trigger表示该元素需要被滚动监听.animate--slide-in启用滑入动画data-cascadeJS 识别“需设置动画顺序”的标志--animation-orderCSS 自定义属性用于计算延迟时间如第 2 个元素延迟 150ms。 CSS 动画定义1234567891011121314151617181920212223: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: slideInvar(--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)尊重用户偏好对晕动症用户更友好。️ JavaScriptIntersection Observer 监听逻辑为什么不用scroll事件传统方式12// ❌ 性能差频繁触发window.addEventListener(scroll, checkVisibility);现代方案12// ✅ 高性能浏览器底层优化constobserver newIntersectionObserver(callback, options);完整监听逻辑1234567891011121314151617181920212223242526272829303132333435363738394041constSCROLL_ANIMATION_TRIGGER_CLASSNAME scroll-trigger;constSCROLL_ANIMATION_OFFSCREEN_CLASSNAME scroll-trigger--offscreen;function onIntersection(entries, observer) {entries.forEach((entry, index) {constel 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) {consttriggers root.querySelectorAll(.${SCROLL_ANIMATION_TRIGGER_CLASSNAME});if(!triggers.length)return;constobserver newIntersectionObserver(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动画只播放一次避免重复触发节省资源。 两种场景下的行为对比这正是你感受到的“丝滑感”来源无论用户如何进入页面动画总是在最合适的时机出现。 总结这套方案的优势