CSS 捲動驅動動畫:零 JS 實作視差捲動、進度條和揭示效果
前端工程(更新於 2026年6月2日)
告別 scroll 事件監聽
傳統捲動動畫依賴 JS 監聽 scroll 事件:
// ❌ 傳統方式:JS 監聽 + 強制同步佈局
window.addEventListener('scroll', () => {
const progress = window.scrollY / (document.body.scrollHeight - window.innerHeight);
progressBar.style.width = `${progress * 100}%`;
// 每次 scroll 都觸發重排!
});
CSS 捲動驅動動畫讓這一切用純 CSS 完成,執行在合成執行緒,不阻塞主執行緒:
/* ✅ 純 CSS:閱讀進度條 */
.progress-bar {
width: 100%;
height: 3px;
background: blue;
transform-origin: left;
animation: progress linear;
animation-timeline: scroll();
}
@keyframes progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
scroll-timeline 語法
匿名捲動時間線
/* 基於最近可捲動祖先的捲動 */
.animated {
animation-timeline: scroll();
}
/* 指定方向 */
.animated-x {
animation-timeline: scroll(x); /* 水平捲動 */
}
.animated-y {
animation-timeline: scroll(y); /* 垂直捲動 */
}
命名捲動時間線
/* 在捲動容器上宣告時間線名稱 */
.scroll-container {
scroll-timeline: --page-scroll y;
overflow-y: auto;
}
/* 在動畫元素上引用 */
.animated-element {
animation: fade-in linear;
animation-timeline: --page-scroll;
}
view-timeline 語法
view-timeline 基於元素進入/離開視口的進度:
.reveal-item {
view-timeline: --item-reveal;
animation: reveal linear both;
animation-timeline: --item-reveal;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
view-timeline-range:精確控制動畫範圍
.reveal-item {
view-timeline: --item-reveal;
animation: reveal linear both;
animation-timeline: --item-reveal;
/* 只在元素進入視口的前 30% 播放動畫 */
animation-range: entry 0% entry 30%;
}
| range 值 | 含義 |
|---|---|
entry |
元素開始進入視口 |
exit |
元素開始離開視口 |
cover |
元素完全在視口內 |
contain |
元素從完全進入到開始離開 |
entry-crossing |
元素正在穿越視口邊緣進入 |
實戰一:視差捲動
.parallax-container {
overflow-y: auto;
scroll-timeline: --parallax y;
height: 100vh;
}
.parallax-bg {
animation: parallax-move linear;
animation-timeline: --parallax;
}
@keyframes parallax-move {
from { transform: translateY(0); }
to { transform: translateY(-200px); }
}
/* 不同深度的視差速度 */
.parallax-layer-1 { animation: parallax-move-slow linear; animation-timeline: --parallax; }
.parallax-layer-2 { animation: parallax-move-medium linear; animation-timeline: --parallax; }
.parallax-layer-3 { animation: parallax-move-fast linear; animation-timeline: --parallax; }
@keyframes parallax-move-slow { to { transform: translateY(-50px); } }
@keyframes parallax-move-medium { to { transform: translateY(-150px); } }
@keyframes parallax-move-fast { to { transform: translateY(-300px); } }
實戰二:閱讀進度條
/* 頁面頂部的閱讀進度指示器 */
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
transform-origin: left;
z-index: 1000;
animation: reading-progress linear;
animation-timeline: scroll(root y);
}
@keyframes reading-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
實戰三:捲動揭示(Scroll Reveal)
/* 從下方淡入 */
.reveal-up {
view-timeline: --reveal;
animation: reveal-up linear both;
animation-timeline: --reveal;
animation-range: entry 0% entry 40%;
}
@keyframes reveal-up {
from {
opacity: 0;
transform: translateY(80px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 從左側滑入 */
.reveal-left {
view-timeline: --reveal;
animation: reveal-left linear both;
animation-timeline: --reveal;
animation-range: entry 0% entry 30%;
}
@keyframes reveal-left {
from {
opacity: 0;
transform: translateX(-60px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
交錯延遲
/* 用 animation-delay 實作交錯效果 */
.stagger-item:nth-child(1) { animation-delay: 0ms; }
.stagger-item:nth-child(2) { animation-delay: 80ms; }
.stagger-item:nth-child(3) { animation-delay: 160ms; }
.stagger-item:nth-child(4) { animation-delay: 240ms; }
.stagger-item:nth-child(5) { animation-delay: 320ms; }
實戰四:水平捲動區域
.horizontal-scroll {
overflow-x: auto;
scroll-timeline: --hscroll x;
}
.card {
animation: card-appear linear both;
animation-timeline: --hscroll;
animation-range: entry 0% entry 20%;
}
@keyframes card-appear {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
實戰五:粘性頭部收縮
.page {
scroll-timeline: --page y;
overflow-y: auto;
}
.sticky-header {
position: sticky;
top: 0;
animation: header-shrink linear both;
animation-timeline: --page;
animation-range: 0px 100px;
}
@keyframes header-shrink {
from {
padding: 24px;
font-size: 2rem;
}
to {
padding: 12px;
font-size: 1.25rem;
}
}
實戰六:圖片序列幀動畫
.sprite-container {
scroll-timeline: --sprite y;
overflow-y: auto;
}
.sprite-image {
animation: sprite-steps steps(24) both;
animation-timeline: --sprite;
}
@keyframes sprite-steps {
from { background-position: 0 0; }
to { background-position: -2400px 0; } /* 24幀 × 100px寬 */
}
JS vs CSS 捲動動畫對比
| 維度 | JS (scroll 事件) | CSS scroll-timeline |
|---|---|---|
| 效能 | 主執行緒,可能卡頓 | 合成執行緒,60fps |
| 程式碼量 | 20-50 行 | 5-10 行 |
| 相容性 | 全部瀏覽器 | Chrome 115+, Firefox 125+ |
| 靈活性 | 任意邏輯 | 線性/分段動畫 |
| 除錯 | console.log | DevTools Animations 面板 |
| 記憶體 | 需手動清理監聽器 | 自動管理 |
Polyfill 策略
// 檢測支援
const supportsScrollTimeline = CSS.supports('animation-timeline', 'scroll()');
if (!supportsScrollTimeline) {
// 降級到 IntersectionObserver + CSS 類別切換
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.classList.toggle('in-view', entry.isIntersecting);
});
}, { threshold: 0.2 });
document.querySelectorAll('.reveal-item').forEach(el => {
observer.observe(el);
});
}
瀏覽器相容性
| 瀏覽器 | scroll-timeline | view-timeline | animation-range |
|---|---|---|---|
| Chrome 115+ | ✅ | ✅ | ✅ |
| Edge 115+ | ✅ | ✅ | ✅ |
| Firefox 125+ | ✅ | ✅ | ✅ |
| Safari 17.4+ | ❌ | ❌ | ❌ |
Safari 仍在實作中,建議使用特性檢測 + IntersectionObserver 降級。
最佳實踐
- 優先使用 view-timeline:大多數捲動揭示效果用
view-timeline比scroll-timeline更簡潔 - animation-range 精確控制:避免動畫在不可見時仍在執行
- will-change 配合:對動畫元素添加
will-change: transform, opacity - 減少同時動畫元素:超過 20 個同時執行的捲動動畫可能影響效能
- 特性檢測降級:Safari 不支援時用 IntersectionObserver 降級
總結
CSS 捲動驅動動畫是 2026 年前端最令人興奮的新特性之一。它讓視差捲動、閱讀進度條、捲動揭示等經典效果從 JS 回歸 CSS,效能提升顯著(合成執行緒執行,不阻塞主執行緒)。雖然 Safari 支援尚不完善,但配合 IntersectionObserver 降級,現在就可以在生產環境使用。
本站提供瀏覽器本地工具,免註冊即可試用 →
#滚动驱动动画#CSS动画#scroll-timeline#视差滚动#无JS动画