捲動式敘事

網頁功能: 捲動驅動動畫

Scrollytelling 是一種熱門技術,可用來打造引人入勝的沉浸式網頁體驗。這種技術會在使用者捲動頁面時,為頁面元素加上動畫效果,有效傳達故事或引導使用者瀏覽敘事內容。有了 CSS 捲動式動畫,您就能直接在 CSS 中建立這些效果,不必依賴 JavaScript。動畫是由捲動位置控制,而非時間,因此能確保動畫一律與使用者的捲動動作同步。

導入方式

如要建立捲動敘事體驗,您需要兩組元素:一組用於追蹤捲動位置,另一組用於動畫。

首先,請在要追蹤的元素上定義已命名的 view-timeline。這些會做為動畫的驅動程式。

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

接著,將動畫套用至要建立動畫的元素,並使用 animation-timeline 屬性將這些元素連結至您剛建立的時間軸。

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

animation-timeline 必須與具名時間軸位於相同範圍,才能參照這些時間軸。您可以在共同祖系上使用 timeline-scope 屬性,讓所有需要時間軸的元素都能使用時間軸。:root 元素通常是合適的選擇。

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

最後,您可以使用 animation-range 屬性,指定動畫應執行的確切時間軸範圍。這樣一來,您就能精細控管動畫的觸發時機和進度。

#animated section {
  animation-range: entry 25% cover 50%, exit 50% exit 75%;
}

範例程式碼

html {
  timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}

#tracked {
  section:nth-child(1){ view-timeline: --tl-1 block; }
  section:nth-child(2){ view-timeline: --tl-2 block; }
  section:nth-child(3){ view-timeline: --tl-3 block; }
  section:nth-child(4){ view-timeline: --tl-4 block; }
  section:nth-child(5){ view-timeline: --tl-5 block; }
}

@keyframes animate-in {
  from { scale: 0.5; opacity: 0; transform: rotateY(-180deg); }
  to { transform: rotateY(0deg); }
}
@keyframes animate-out {
  to { translate: 100% 0; opacity: 0; }
}

#animated {
  section {
    animation: animate-in auto linear both, animate-out auto linear forwards;
    animation-range: entry 25% cover 50%, exit 50% exit 75%;
    backface-visibility: hidden;
  }

  section:nth-child(1){ animation-timeline: --tl-1; }
  section:nth-child(2){ animation-timeline: --tl-2; }
  section:nth-child(3){ animation-timeline: --tl-3; }
  section:nth-child(4){ animation-timeline: --tl-4; }
  section:nth-child(5){ animation-timeline: --tl-5; }
}

/* MANDATORY Copy-Paste Safety: Disable continuous storytelling motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

最佳做法

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

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

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

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

備用策略

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

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

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

針對這個用途,下列指令碼會為不支援捲動驅動動畫的瀏覽器套用備用方案。它會使用 IntersectionObserver 追蹤每個 #tracked section 元素的可見度,並據此更新對應 #animated section 的 transform 屬性。

const animatedSections = document.querySelectorAll('#animated section');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    const sectionIndex = Array.from(document.querySelectorAll('#tracked section')).indexOf(entry.target);
    if (sectionIndex !== -1) {
      const animatedSection = animatedSections[sectionIndex];
      const ratio = entry.intersectionRatio;

      // Animate-in
      animatedSection.style.opacity = ratio;
      animatedSection.style.transform = `scale(${0.5 + ratio * 0.5}) rotateY(${-180 + ratio * 180}deg)`;

      // Animate-out
      if (ratio < 0.5) {
        animatedSection.style.translate = `${(0.5 - ratio) * 2 * 100}% 0`;
      } else {
        animatedSection.style.translate = '0 0';
      }
    }
  });
}, { threshold: Array.from({length: 101}, (_, i) => i / 100) });

document.querySelectorAll('#tracked section').forEach(section => {
  observer.observe(section);
});

以及隨附的 CSS:

#animated section {
  opacity: 0;
  transform: scale(0.5)  rotateY(-180deg);
  backface-visibility: hidden;
}

/* MANDATORY Copy-Paste Safety: Ensure content remains fully visible and legible for assistive technologies or users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
  #animated section {
    opacity: 1 !important;
    transform: none !important;
    translate: 0 0 !important;
  }
}

對於不支援原生 CSS 功能的瀏覽器,這個備用方案可提供更準確的捲動驅動動畫,確保所有使用者都能享有更一致的體驗。只要為 IntersectionObserver 使用一系列門檻,就能更精確地追蹤捲動位置,並建立更流暢的動畫。