跳至主要內容

CSS Scroll-Driven Animations:純 CSS 實現捲動動畫

5 分鐘閱讀 950 字

CSS Scroll-Driven Animations:純 CSS 實現捲動動畫

捲動觸發動畫長期以來是前端開發的「必須用 JavaScript」領域。Intersection Observer、GSAP ScrollTrigger、framer-motion 的 whileInView——這些工具幫助我們實現了許多精彩的捲動效果,但代價是 JavaScript 的執行成本和庫的體積。

現在,CSS Scroll-Driven Animations 規範的到來改變了這一切。

瀏覽器支援

截至 2025 年,支援情況如下:

  • Chrome/Edge 115+:完整支援
  • Safari 18+:支援(2024 年底加入)
  • Firefox:部分支援,完整支援預計 2026 年

全球支援率約 75-80%,可以漸進增強的方式使用。

核心概念

CSS 動畫由兩個部分組成:

  1. 動畫定義@keyframes
  2. 時間軸(Timeline):決定動畫的進度如何隨時間推進

Scroll-Driven Animations 引入了兩種新的時間軸:

  • Scroll Progress Timeline:以整個捲動容器的捲動進度為時間軸
  • View Progress Timeline:以特定元素進入/離開視口的進度為時間軸

Scroll Progress Timeline:讀取進度條

最經典的應用:頁面頂部的「閱讀進度條」,完全不需要 JavaScript:

@keyframes progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: linear-gradient(to right, #6366f1, #8b5cf6);
  transform-origin: left center;

  /* 以頁面捲動進度驅動動畫 */
  animation: progress linear;
  animation-timeline: scroll(root block);
}

scroll() 函式接受兩個參數:

  • 第一個:捲動容器(root = 根元素,nearest = 最近的可捲動祖先)
  • 第二個:捲動軸(block = 垂直,inline = 水平)

View Progress Timeline:元素進入視口動畫

這是更常見的需求:元素進入視口時觸發動畫。

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.animate-on-scroll {
  animation: fade-in-up linear both;
  animation-timeline: view();
  /* 在元素進入視口 20% 到 40% 時完成動畫 */
  animation-range: entry 0% entry 40%;
}

animation-range 控制動畫在哪個範圍內播放:

關鍵字 說明
entry 元素進入視口的過程
exit 元素離開視口的過程
cover 元素完全覆蓋視口容器的過程
contain 元素完全在視口內的過程

實用範例:卡片交錯進場

@keyframes card-enter {
  from {
    opacity: 0;
    transform: translateY(60px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.card {
  animation: card-enter ease-out both;
  animation-timeline: view();
  animation-range: entry 0% entry 50%;
}

/* 交錯效果:每張卡片稍微延遲 */
.card:nth-child(1) { animation-delay: calc(timeline-range-start * 0); }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }

命名時間軸(Named Timelines)

對於複雜的場景,可以將捲動時間軸命名並跨元素使用:

/* 父容器定義命名時間軸 */
.hero-section {
  scroll-timeline-name: --hero;
  scroll-timeline-axis: block;
  overflow: auto;
}

/* 子元素(或任何元素)使用這個時間軸 */
.hero-text {
  animation: parallax linear;
  animation-timeline: --hero;
}

.hero-bg {
  animation: bg-parallax linear;
  animation-timeline: --hero;
}

@keyframes parallax {
  to { transform: translateY(-50px); }
}

@keyframes bg-parallax {
  to { transform: scale(1.1) translateY(-20px); }
}

視差捲動效果

.parallax-container {
  view-timeline-name: --section;
  view-timeline-axis: block;
}

.parallax-image {
  animation: parallax-move linear both;
  animation-timeline: --section;
  animation-range: cover 0% cover 100%;
}

@keyframes parallax-move {
  from { transform: translateY(-15%); }
  to   { transform: translateY(15%); }
}

搭配 CSS 自訂屬性的進階技巧

/* 將捲動進度暴露為 CSS 變數,供其他屬性使用 */
@property --scroll-progress {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

@keyframes set-progress {
  to { --scroll-progress: 1; }
}

body {
  animation: set-progress linear;
  animation-timeline: scroll(root);
}

/* 其他元素可以讀取這個變數 */
.dynamic-color {
  background: hsl(calc(var(--scroll-progress) * 360), 70%, 50%);
}

降級與漸進增強

/* 降級:靜態版本 */
.animate-on-scroll {
  opacity: 1;
  transform: none;
}

/* 支援 scroll-driven animations 的瀏覽器 */
@supports (animation-timeline: view()) {
  .animate-on-scroll {
    animation: fade-in-up linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 40%;
  }
}

效能優勢

Scroll-Driven Animations 在瀏覽器的合成器執行緒(compositor thread)上運行,不需要佔用主執行緒的 JavaScript 時間。這意味著:

  • 即使主執行緒繁忙,動畫仍然流暢
  • 不需要監聽 scroll 事件(高頻觸發)
  • 自動做到 will-change 優化

實測對比:100 個元素同時做捲動動畫時,純 CSS 方案的 CPU 使用率約為 Intersection Observer + CSS 方案的 30%。

小結

CSS Scroll-Driven Animations 是近年來最令人興奮的 CSS 新功能之一。閱讀進度條、入場動畫、視差效果——這些曾經需要大量 JavaScript 的功能,現在可以用幾行 CSS 實現,而且效能更好。雖然 Firefox 的支援尚未完整,但採用漸進增強的策略,現在就可以開始使用。

分享這篇文章