scroll-driven-animations
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseScroll-Driven Animations
滚动驱动动画
CSS Scroll-Driven Animations API provides performant, declarative scroll-linked animations without JavaScript. Supported in Chrome 115+, Edge 115+, Safari 18.4+.
CSS滚动驱动动画API无需JavaScript即可实现高性能、声明式的滚动关联动画。支持Chrome 115+、Edge 115+、Safari 18.4+浏览器。
Overview
概述
- Progress indicators tied to scroll position
- Parallax effects without JavaScript jank
- Element reveal animations on scroll into view
- Sticky header animations based on scroll
- Reading progress bars
- Scroll-triggered image/content reveals
- 与滚动位置绑定的进度指示器
- 无JavaScript卡顿的视差效果
- 元素进入视口时的显示动画
- 基于滚动的粘性头部动画
- 阅读进度条
- 滚动触发的图片/内容显示
Core Concepts
核心概念
Timeline Types
时间线类型
| Timeline | CSS Function | Use Case |
|---|---|---|
| Scroll Progress | | Tied to scroll container position (0-100%) |
| View Progress | | Tied to element visibility in viewport |
| 时间线类型 | CSS函数 | 适用场景 |
|---|---|---|
| 滚动进度 | | 与滚动容器位置绑定(0-100%) |
| 视图进度 | | 与元素在视口中的可见性绑定 |
CSS Patterns
CSS模式
1. Scroll Progress Timeline (Reading Progress)
1. 滚动进度时间线(阅读进度条)
css
/* Progress bar that fills as page scrolls */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: var(--color-primary);
transform-origin: left;
/* Animate based on root scroll */
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}css
/* Progress bar that fills as page scrolls */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: var(--color-primary);
transform-origin: left;
/* Animate based on root scroll */
animation: grow-progress linear;
animation-timeline: scroll(root block);
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}2. View Timeline (Reveal on Scroll)
2. 视图进度时间线(滚动显示)
css
/* Fade in when element enters viewport */
.reveal-on-scroll {
animation: fade-slide-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fade-slide-up {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}css
/* Fade in when element enters viewport */
.reveal-on-scroll {
animation: fade-slide-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fade-slide-up {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}3. Animation Range Control
3. 动画范围控制
css
/* Fine-tune when animation runs */
.card {
animation: scale-up linear both;
animation-timeline: view();
/* Start at 25% entry, complete at 75% entry */
animation-range: entry 25% entry 75%;
}
/* Full visibility animation */
.hero-image {
animation: parallax linear both;
animation-timeline: view();
/* Animate through entire visibility */
animation-range: cover 0% cover 100%;
}
@keyframes parallax {
from { transform: translateY(-20%); }
to { transform: translateY(20%); }
}css
/* Fine-tune when animation runs */
.card {
animation: scale-up linear both;
animation-timeline: view();
/* Start at 25% entry, complete at 75% entry */
animation-range: entry 25% entry 75%;
}
/* Full visibility animation */
.hero-image {
animation: parallax linear both;
animation-timeline: view();
/* Animate through entire visibility */
animation-range: cover 0% cover 100%;
}
@keyframes parallax {
from { transform: translateY(-20%); }
to { transform: translateY(20%); }
}4. Named Scroll Timelines
4. 命名滚动时间线
css
/* Define timeline on scroll container */
.scroll-container {
overflow-y: auto;
scroll-timeline-name: --container-scroll;
scroll-timeline-axis: block;
}
/* Use timeline in descendant */
.progress-indicator {
animation: progress linear;
animation-timeline: --container-scroll;
}
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}css
/* Define timeline on scroll container */
.scroll-container {
overflow-y: auto;
scroll-timeline-name: --container-scroll;
scroll-timeline-axis: block;
}
/* Use timeline in descendant */
.progress-indicator {
animation: progress linear;
animation-timeline: --container-scroll;
}
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}5. Named View Timelines with Scope
5. 带作用域的命名视图时间线
css
/* Parent sets up the timeline scope */
.gallery {
timeline-scope: --card-timeline;
}
/* Each card defines its view timeline */
.gallery-card {
view-timeline-name: --card-timeline;
view-timeline-axis: block;
}
/* Animate based on card visibility */
.gallery-card .image {
animation: zoom-in linear both;
animation-timeline: --card-timeline;
animation-range: entry 0% cover 50%;
}
@keyframes zoom-in {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}css
/* Parent sets up the timeline scope */
.gallery {
timeline-scope: --card-timeline;
}
/* Each card defines its view timeline */
.gallery-card {
view-timeline-name: --card-timeline;
view-timeline-axis: block;
}
/* Animate based on card visibility */
.gallery-card .image {
animation: zoom-in linear both;
animation-timeline: --card-timeline;
animation-range: entry 0% cover 50%;
}
@keyframes zoom-in {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}6. Parallax Sections
6. 视差区块
css
.parallax-section {
position: relative;
overflow: hidden;
}
.parallax-bg {
position: absolute;
inset: -20% 0;
animation: parallax-scroll linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
}
@keyframes parallax-scroll {
from { transform: translateY(0); }
to { transform: translateY(40%); }
}css
.parallax-section {
position: relative;
overflow: hidden;
}
.parallax-bg {
position: absolute;
inset: -20% 0;
animation: parallax-scroll linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
}
@keyframes parallax-scroll {
from { transform: translateY(0); }
to { transform: translateY(40%); }
}7. Sticky Header Animation
7. 粘性头部动画
css
.header {
position: sticky;
top: 0;
animation: shrink-header linear both;
animation-timeline: scroll(root);
animation-range: 0px 200px;
}
@keyframes shrink-header {
from {
padding-block: 2rem;
background: transparent;
}
to {
padding-block: 0.5rem;
background: var(--color-surface);
box-shadow: var(--shadow-md);
}
}css
.header {
position: sticky;
top: 0;
animation: shrink-header linear both;
animation-timeline: scroll(root);
animation-range: 0px 200px;
}
@keyframes shrink-header {
from {
padding-block: 2rem;
background: transparent;
}
to {
padding-block: 0.5rem;
background: var(--color-surface);
box-shadow: var(--shadow-md);
}
}JavaScript API
JavaScript API
ScrollTimeline
ScrollTimeline
typescript
// Create scroll timeline programmatically
const scrollTimeline = new ScrollTimeline({
source: document.documentElement, // or specific scroll container
axis: 'block', // 'block' | 'inline' | 'x' | 'y'
});
// Attach to animation
element.animate(
[
{ transform: 'translateY(100px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 },
],
{
timeline: scrollTimeline,
fill: 'both',
}
);typescript
// Create scroll timeline programmatically
const scrollTimeline = new ScrollTimeline({
source: document.documentElement, // or specific scroll container
axis: 'block', // 'block' | 'inline' | 'x' | 'y'
});
// Attach to animation
element.animate(
[
{ transform: 'translateY(100px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 },
],
{
timeline: scrollTimeline,
fill: 'both',
}
);ViewTimeline
ViewTimeline
typescript
// Create view timeline for specific element
const viewTimeline = new ViewTimeline({
subject: element, // Element to track
axis: 'block',
inset: [CSS.px(0), CSS.px(0)], // Optional viewport inset
});
// Animate based on element visibility
element.animate(
[
{ opacity: 0, transform: 'scale(0.8)' },
{ opacity: 1, transform: 'scale(1)' },
],
{
timeline: viewTimeline,
fill: 'both',
rangeStart: 'entry 0%',
rangeEnd: 'cover 50%',
}
);typescript
// Create view timeline for specific element
const viewTimeline = new ViewTimeline({
subject: element, // Element to track
axis: 'block',
inset: [CSS.px(0), CSS.px(0)], // Optional viewport inset
});
// Animate based on element visibility
element.animate(
[
{ opacity: 0, transform: 'scale(0.8)' },
{ opacity: 1, transform: 'scale(1)' },
],
{
timeline: viewTimeline,
fill: 'both',
rangeStart: 'entry 0%',
rangeEnd: 'cover 50%',
}
);React Integration
React集成
tsx
import { useRef, useEffect } from 'react';
function useScrollAnimation(
keyframes: Keyframe[],
options: {
timeline?: 'scroll' | 'view';
range?: string;
} = {}
) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = ref.current;
if (!element || !('animate' in element)) return;
// Feature detection
if (!('ScrollTimeline' in window)) {
console.warn('Scroll-driven animations not supported');
return;
}
const timeline = options.timeline === 'view'
? new ViewTimeline({ subject: element, axis: 'block' })
: new ScrollTimeline({ source: document.documentElement, axis: 'block' });
const animation = element.animate(keyframes, {
timeline,
fill: 'both',
...(options.range && {
rangeStart: options.range.split(' ')[0],
rangeEnd: options.range.split(' ')[1],
}),
});
return () => animation.cancel();
}, [keyframes, options.timeline, options.range]);
return ref;
}
// Usage
function RevealCard({ children }: { children: React.ReactNode }) {
const ref = useScrollAnimation(
[
{ opacity: 0, transform: 'translateY(50px)' },
{ opacity: 1, transform: 'translateY(0)' },
],
{ timeline: 'view', range: 'entry cover' }
);
return <div ref={ref}>{children}</div>;
}tsx
import { useRef, useEffect } from 'react';
function useScrollAnimation(
keyframes: Keyframe[],
options: {
timeline?: 'scroll' | 'view';
range?: string;
} = {}
) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = ref.current;
if (!element || !('animate' in element)) return;
// Feature detection
if (!('ScrollTimeline' in window)) {
console.warn('Scroll-driven animations not supported');
return;
}
const timeline = options.timeline === 'view'
? new ViewTimeline({ subject: element, axis: 'block' })
: new ScrollTimeline({ source: document.documentElement, axis: 'block' });
const animation = element.animate(keyframes, {
timeline,
fill: 'both',
...(options.range && {
rangeStart: options.range.split(' ')[0],
rangeEnd: options.range.split(' ')[1],
}),
});
return () => animation.cancel();
}, [keyframes, options.timeline, options.range]);
return ref;
}
// Usage
function RevealCard({ children }: { children: React.ReactNode }) {
const ref = useScrollAnimation(
[
{ opacity: 0, transform: 'translateY(50px)' },
{ opacity: 1, transform: 'translateY(0)' },
],
{ timeline: 'view', range: 'entry cover' }
);
return <div ref={ref}>{children}</div>;
}Progressive Enhancement
渐进式增强
css
/* Fallback for unsupported browsers */
.reveal-on-scroll {
opacity: 1; /* Default visible */
transform: translateY(0);
}
/* Apply animation only when supported */
@supports (animation-timeline: view()) {
.reveal-on-scroll {
animation: fade-slide-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}tsx
// Feature detection in React
const supportsScrollTimeline =
typeof ScrollTimeline !== 'undefined';
function AnimatedSection({ children }: { children: React.ReactNode }) {
if (!supportsScrollTimeline) {
// Fallback: use Intersection Observer
return <IntersectionObserverFallback>{children}</IntersectionObserverFallback>;
}
return <ScrollAnimatedSection>{children}</ScrollAnimatedSection>;
}css
/* Fallback for unsupported browsers */
.reveal-on-scroll {
opacity: 1; /* Default visible */
transform: translateY(0);
}
/* Apply animation only when supported */
@supports (animation-timeline: view()) {
.reveal-on-scroll {
animation: fade-slide-up linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}tsx
// Feature detection in React
const supportsScrollTimeline =
typeof ScrollTimeline !== 'undefined';
function AnimatedSection({ children }: { children: React.ReactNode }) {
if (!supportsScrollTimeline) {
// Fallback: use Intersection Observer
return <IntersectionObserverFallback>{children}</IntersectionObserverFallback>;
}
return <ScrollAnimatedSection>{children}</ScrollAnimatedSection>;
}Chrome DevTools Debugging
Chrome DevTools调试
- Open DevTools → Elements tab
- Find "Scroll-Driven Animations" tab (may be in overflow ››)
- Select element with scroll animation
- Scrub timeline to preview animation
- Inspect animation-timeline and animation-range values
- 打开DevTools → 元素标签页
- 找到“滚动驱动动画”标签(可能在更多选项 ›› 中)
- 选择带有滚动动画的元素
- 拖动时间线预览动画
- 检查animation-timeline和animation-range属性值
Performance Best Practices
性能最佳实践
css
/* ✅ CORRECT: Animate transform/opacity only */
@keyframes good-animation {
from { transform: translateY(100px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ❌ WRONG: Animate layout properties */
@keyframes bad-animation {
from { margin-top: 100px; height: 0; }
to { margin-top: 0; height: auto; }
}
/* ✅ Use will-change sparingly */
.scroll-animated {
will-change: transform, opacity;
}css
/* ✅ CORRECT: Animate transform/opacity only */
@keyframes good-animation {
from { transform: translateY(100px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ❌ WRONG: Animate layout properties */
@keyframes bad-animation {
from { margin-top: 100px; height: 0; }
to { margin-top: 0; height: auto; }
}
/* ✅ Use will-change sparingly */
.scroll-animated {
will-change: transform, opacity;
}Anti-Patterns (FORBIDDEN)
反模式(禁止)
css
/* ❌ NEVER: Animate layout-triggering properties */
@keyframes bad {
from { width: 0; margin-left: 100px; }
to { width: 100%; margin-left: 0; }
}
/* ❌ NEVER: Use without fallback */
.element {
animation-timeline: scroll(); /* Breaks in Firefox! */
}
/* ❌ NEVER: Overly complex animation chains */
.element {
animation: anim1, anim2, anim3, anim4, anim5;
animation-timeline: view(), scroll(), view(), scroll(), view();
}
/* ❌ NEVER: Scroll animations on non-scrollable containers */
.no-overflow {
overflow: hidden;
scroll-timeline-name: --timeline; /* Won't work! */
}css
/* ❌ NEVER: Animate layout-triggering properties */
@keyframes bad {
from { width: 0; margin-left: 100px; }
to { width: 100%; margin-left: 0; }
}
/* ❌ NEVER: Use without fallback */
.element {
animation-timeline: scroll(); /* Breaks in Firefox! */
}
/* ❌ NEVER: Overly complex animation chains */
.element {
animation: anim1, anim2, anim3, anim4, anim5;
animation-timeline: view(), scroll(), view(), scroll(), view();
}
/* ❌ NEVER: Scroll animations on non-scrollable containers */
.no-overflow {
overflow: hidden;
scroll-timeline-name: --timeline; /* Won't work! */
}Browser Support
浏览器支持
| Browser | scroll() | view() | ScrollTimeline API |
|---|---|---|---|
| Chrome 115+ | ✅ | ✅ | ✅ |
| Edge 115+ | ✅ | ✅ | ✅ |
| Safari 18.4+ | ✅ | ✅ | ✅ |
| Firefox | ❌ | ❌ | ❌ (in development) |
| 浏览器 | scroll() | view() | ScrollTimeline API |
|---|---|---|---|
| Chrome 115+ | ✅ | ✅ | ✅ |
| Edge 115+ | ✅ | ✅ | ✅ |
| Safari 18.4+ | ✅ | ✅ | ✅ |
| Firefox | ❌ | ❌ | ❌(开发中) |
Key Decisions
关键决策
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| Timeline type | scroll() | view() | view() for reveals, scroll() for progress |
| Fallback strategy | IntersectionObserver | No animation | IntersectionObserver fallback |
| Animation properties | All CSS | transform/opacity | transform/opacity only |
| Range units | Percentages | Named ranges | Named ranges (entry, cover) for clarity |
| 决策项 | 选项A | 选项B | 推荐方案 |
|---|---|---|---|
| 时间线类型 | scroll() | view() | **view()**用于显示动画,**scroll()**用于进度条 |
| 降级策略 | IntersectionObserver | 无动画 | IntersectionObserver降级方案 |
| 动画属性 | 所有CSS属性 | transform/opacity | 仅使用transform/opacity |
| 范围单位 | 百分比 | 命名范围 | 命名范围(entry、cover)更清晰 |
Related Skills
相关技能
- - Framer Motion for JS animations
motion-animation-patterns - - Performance impact considerations
core-web-vitals - - Complementary page transitions
view-transitions
- - 用于JavaScript动画的Framer Motion
motion-animation-patterns - - 性能影响考量
core-web-vitals - - 互补的页面过渡效果
view-transitions
Capability Details
能力详情
scroll-timeline
scroll-timeline
Keywords: scroll(), progress, scroll position, reading progress
Solves: Animations tied to scroll container position
关键词: scroll(), progress, scroll position, reading progress
解决问题: 与滚动容器位置绑定的动画
view-timeline
view-timeline
Keywords: view(), visibility, reveal, enter viewport
Solves: Animations triggered by element visibility
关键词: view(), visibility, reveal, enter viewport
解决问题: 由元素可见性触发的动画
parallax-effects
parallax-effects
Keywords: parallax, background, depth, scroll speed
Solves: Performant parallax without JavaScript
关键词: parallax, background, depth, scroll speed
解决问题: 无需JavaScript的高性能视差效果
scroll-triggered
scroll-triggered
Keywords: trigger, intersection, enter, exit, reveal
Solves: Trigger animations on scroll position
关键词: trigger, intersection, enter, exit, reveal
解决问题: 由滚动位置触发的动画
progressive-enhancement
progressive-enhancement
Keywords: fallback, @supports, feature detection
Solves: Support for browsers without scroll-driven animations
关键词: fallback, @supports, feature detection
解决问题: 为不支持滚动驱动动画的浏览器提供兼容方案
References
参考资料
- - CSS scroll() and view() functions
references/css-scroll-timeline.md - - JavaScript ScrollTimeline/ViewTimeline API
references/js-api.md - - React parallax component
scripts/parallax-section.tsx
- - CSS scroll()和view()函数
references/css-scroll-timeline.md - - JavaScript ScrollTimeline/ViewTimeline API
references/js-api.md - - React视差组件
scripts/parallax-section.tsx