小人憮 發表於 2026-4-20 10:12:00

用 Intersection Observer 打造丝滑的级联滚动动画

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<blockquote>
<p>无需任何动画库,仅用原生 Web API 实现滚动时丝滑的淡入滑入效果,兼顾性能与体验。</p>
</blockquote>
<p>你是否见过这样的交互动效:</p>
<ul>
<li>用户滚动页面时,一组卡片像被“唤醒”一样,依次从下方滑入并淡入;</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202604/2149129-20260420100913056-1110489823.png" alt="2026042002" loading="lazy"></p>
<p>&nbsp;</p>
<ul>
<li>如果这些元素在页面加载时已在视口内,它们也会自动按顺序浮现。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202604/2149129-20260420100927685-1519906152.png" alt="2026042001" loading="lazy"></p>
<div>
<div>
<p>这种效果不仅视觉流畅,还能有效引导用户注意力,提升内容层次感。更重要的是——<strong>它不依赖 GSAP、AOS 等第三方库</strong>,仅靠 <code>Intersection Observer</code> + <code>CSS 动画</code> + 少量 JavaScript,就能实现<strong>高性能、可访问、且高度可控</strong>的滚动触发型级联动画。</p>
<p>今天,我们就来一步步拆解这个经典动效,并给出一套<strong>可直接复用的轻量级方案</strong>。</p>
<hr>
<h2 data-id="heading-0">🔧 核心原理概览</h2>
<p>整个动画系统依赖三个关键技术点:</p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202604/2149129-20260420100940913-164806246.png" alt="ScreenShot_2026-04-20_100642_102" loading="lazy"></p>
<p>&nbsp;</p>
<p>最关键的设计哲学是:动画只在用户能看到它的时候才执行,既节省性能,又避免“闪现”。</p>
<hr>
<h2 data-id="heading-1">🧱 HTML 结构(简化版)</h2>
<p>为便于理解,我们剥离业务逻辑,只保留动效核心:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;div class="container"&gt;
    &lt;ul class="card-list"&gt;
      &lt;li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 1;"
            &gt;Card 1&lt;/li
      &gt;
      &lt;li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 2;"
            &gt;Card 2&lt;/li
      &gt;
      &lt;li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 3;"
            &gt;Card 3&lt;/li
      &gt;
      &lt;!-- 更多卡片... --&gt;
    &lt;/ul&gt;
&lt;/div&gt;</pre>
</div>
<div>
<div>
<h3 data-id="heading-2">💡 类名与属性说明</h3>
<ul>
<li><code>.scroll-trigger</code>:表示该元素需要被滚动监听;</li>
<li><code>.animate--slide-in</code>:启用滑入动画;</li>
<li><code>data-cascade</code>:JS 识别“需设置动画顺序”的标志;</li>
<li><code>--animation-order</code>:CSS 自定义属性,用于计算延迟时间(如第 2 个元素延迟 150ms)。</li>
</ul>
<hr>
<h2 data-id="heading-3">🎨 CSS 动画定义</h2>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">: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;
      }
    }
}</pre>
</div>
<h3 data-id="heading-4">✨ 参数说明</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202604/2149129-20260420101014251-527066961.png" alt="ScreenShot_2026-04-20_100650_846" loading="lazy"></p>
<blockquote>
<p>✅&nbsp;无障碍提示:通过&nbsp;<code>@media (prefers-reduced-motion)</code>&nbsp;尊重用户偏好,对晕动症用户更友好。</p>
</blockquote>
<hr>
<h2 data-id="heading-5">🕵️ JavaScript:Intersection Observer 监听逻辑</h2>
<h3 data-id="heading-6">为什么不用&nbsp;<code>scroll</code>&nbsp;事件?</h3>
<p>传统方式:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// ❌ 性能差,频繁触发
window.addEventListener('scroll', checkVisibility);</pre>
</div>
<p>现代方案:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// ✅ 高性能,浏览器底层优化
const observer = new IntersectionObserver(callback, options);</pre>
</div>
<h3 data-id="heading-7">完整监听逻辑</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const SCROLL_ANIMATION_TRIGGER_CLASSNAME = 'scroll-trigger';
const SCROLL_ANIMATION_OFFSCREEN_CLASSNAME = 'scroll-trigger--offscreen';

function onIntersection(entries, observer) {
    entries.forEach((entry, index) =&gt; {
      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: ,
    });

    triggers.forEach((el) =&gt; observer.observe(el));
}

// 页面加载完成后启动
document.addEventListener('DOMContentLoaded', () =&gt; {
    initScrollAnimations();
});</pre>
</div>
<div>
<div>
<h3 data-id="heading-8">🎯 关键设计细节</h3>
<ul>
<li><code>rootMargin: '0px 0px -50px 0px'</code>:确保元素<strong>完全进入用户视野后再触发动画</strong>,避免“刚看到就结束”;</li>
<li>初始所有 <code>.scroll-trigger</code> 元素默认带有 <code>.scroll-trigger--offscreen</code> 类,阻止 CSS 动画生效;</li>
<li><code>unobserve</code>:动画只播放一次,避免重复触发,节省资源。</li>
</ul>
<hr>
<h2 data-id="heading-9">📊 两种场景下的行为对比</h2>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202604/2149129-20260420101109142-637951667.png" alt="ScreenShot_2026-04-20_100700_286" loading="lazy"></p>
<p>这正是你感受到的“丝滑感”来源:无论用户如何进入页面,动画总是在最合适的时机出现。</p>
<hr>
<h2 data-id="heading-10">💡 总结:这套方案的优势</h2>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202604/2149129-20260420101120099-1138892384.png" alt="ScreenShot_2026-04-20_100708_846" loading="lazy"></p>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19893357
頁: [1]
查看完整版本: 用 Intersection Observer 打造丝滑的级联滚动动画