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动画