為元素新增進入或離開捲動區塊時的效果

網頁功能: 捲動驅動動畫

進場和退場效果是指元素進入或離開可視區域時觸發的動畫。這項功能可用於打造引人入勝的動態使用者體驗。舉例來說,您可以使用進入效果,讓元素在捲動進入檢視畫面時淡入,或使用退出效果,讓元素在捲動離開檢視畫面時縮小。

導入方式

如要為元素新增進入和退出效果,您需要結合多個 CSS 屬性。逐步指南如下:

  1. 為進場和退場動畫建立不同的 @keyframes。元素進入可視區域時會套用進場動畫,離開時則會套用退場動畫。

    @keyframes slide-in {
      from { transform: translateX(-100%); }
    }
    @keyframes slide-out {
      to { transform: translateX(100%); }
    }
    
  2. 將進入和離開關鍵影格附加至元素。方法是在 animation 屬性中定義多個動畫。

    • 將進場動畫的 animation-fill-mode 設為 backwards,這樣動畫就會在開始前套用初始狀態。
    • 將結束動畫的 animation-fill-mode 設為 forwards,讓動畫在完成後維持最終狀態。
    .animated-element {
      animation:
        slide-in 1s linear backwards,
        slide-out 1s linear forwards;
    }
    
  3. 建立 View Timeline 並連結至動畫。檢視區間時間軸是一種時間軸,與可視區域中元素的顯示設定連結。您可以使用 view() 函式建立一個,然後使用 animation-timeline 屬性將其套用至動畫。

    .animated-element {
      animation-timeline: view();
    }
    

    根據預設,view() 會追蹤 block 軸上的元素。如需在 inline 軸上追蹤,可以使用 view(inline)。

  4. 將動畫限制在 entry 和 exit 範圍內。animation-range 屬性可讓您指定動畫應在時間軸的哪個部分執行。

    • entry 範圍涵蓋元素首次進入可視區域到完全顯示的時間。
    • exit 範圍涵蓋元素開始離開可視區域到完全隱藏的時間。
    .animated-element {
      animation-range: entry, exit;
    }
    

範例程式碼

這段程式碼會使用匿名檢視畫面時間軸,在捲動時為捲動器的直接子項加上動畫效果:

@media (prefers-reduced-motion: no-preference) {
  @supports ((animation-timeline: view()) and (animation-range: entry)) {
    @keyframes grow {
      from {
        scale: 0.5;
      }
    }
    @keyframes shrink {
      to {
        scale: 0.5;
      }
    }

    .scroller > * {
      animation:
        grow auto linear backwards,
        shrink auto linear forwards;
      animation-timeline: view(inline);
      animation-range: entry, exit;
    }
  }
}

當元素進入捲動區時,系統會播放 grow 動畫,離開捲動區時則會播放 shrink 動畫。

下列程式碼會產生相同的視覺效果,但會使用具名檢視畫面時間軸,在捲動時為捲動器的直接子項建立動畫:

@media (prefers-reduced-motion: no-preference) {
  @supports ((animation-timeline: view()) and (animation-range: entry)) {
    @keyframes grow {
      from {
        scale: 0.5;
      }
    }
    @keyframes shrink {
      to {
        scale: 0.5;
      }
    }

    .scroller > * {
      view-timeline: --tl inline;
      animation:
        grow auto linear backwards,
        shrink auto linear forwards;
      animation-timeline: --tl;
      animation-range: entry, exit;
    }
  }
}

最佳做法

使用捲動驅動動畫時,請務必遵循幾項最佳做法,確保體驗流暢且無障礙:

  • 務必加入功能偵測:並非所有瀏覽器都支援捲動驅動動畫。使用 @supports ((animation-timeline: view()) and (animation-range: entry)) 檢查支援情形,並為不支援的瀏覽器提供備用方案。
    • 這裡必須加入 (animation-range: entry) 檢查,篩除僅部分支援的瀏覽器。
    • 請勿使用 scroll-timeline-polyfill 套件做為備用策略,因為該套件的功能不完整,且有許多已知問題。
    • 如果動畫僅用於裝飾,請選擇漸進增強,且「不要」提供備用方案。
  • 務必尊重使用者偏好:部分使用者偏好網頁上較少動態效果。使用 prefers-reduced-motion 媒體查詢,為這些使用者停用或減少動畫。
  • 建議只為高效能 CSS 屬性製作動畫:如要讓動畫最流暢,請只為瀏覽器合成器執行緒可處理的屬性製作動畫,例如 transform 和 opacity。為 width 或 height 等其他屬性建立動畫,可能會導致效能問題。
  • 建議使用正確的宣告順序:使用 animation 簡寫屬性時,請在該屬性「之後」宣告 animation-timeline,以免簡寫重設時間軸。

使用 view() 函式建立捲動驅動動畫時:

  • 選用:明確指定要追蹤的軸:如果不是指定預設的 block 軸 (例如在水平捲軸中),請明確指定要追蹤的軸,使用 view(block) 或 view(inline)。
  • 如果動畫未套用至追蹤的主體本身,請使用具名檢視區塊時間軸。

使用 view-timeline 屬性建立捲動驅動動畫時:

  • 建議使用 CSS <dashed-ident> 做為名稱。
  • 選用:明確指定要追蹤的軸。如果不是指定預設的 block 軸 (例如在水平捲動器中),請使用 view-timeline-axis 明確指定要追蹤的軸。
  • 務必確保查詢範圍有效:如果宣告 view-timeline 的元素不是動畫元素的平面樹狀結構祖系,請在共用祖系上使用 timeline-scope,提高 view-timeline 名稱的瀏覽權限。

如果追蹤主體的多個元素或子項需要動畫,建議使用具名 view-timeline。

瀏覽器支援和備用策略

因此,通常需要備用策略。

如果瀏覽器不支援捲動驅動動畫,可以使用備用方案重新建立視覺效果。備用方案通常是使用捲動事件監聽器 (用於 ScrollTimeline 效果) 或 IntersectionObserver API (用於 ViewTimeline 效果) 建構。

如果瀏覽器內建支援捲動式動畫,請一律使用原生 CSS 實作,因為這類實作的效能較佳。

請注意,並非所有效果都能使用備用方法重新建立。

針對這個用途,下列指令碼會為不支援捲動驅動動畫的瀏覽器套用備用方案。這個函式會使用 IntersectionObserver 追蹤 .wrapper 元素的可見度,並根據捲動位置更新圖層的 transform 屬性。

<script>
  if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          // This matches the effect as defined in the CSS example above.
          // Customize this further if needed.
          entry.target.style.scale = 0.5 + entry.intersectionRatio * 0.5;
        }
      },
      {
        threshold: Array.from({ length: 101 }, (_, i) => i / 100),
      }
    );

    document.querySelectorAll('.scroller > *').forEach((el) => {
      observer.observe(el);
    });
  }
</script>