scroll-driven-animations

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Scroll-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

时间线类型

TimelineCSS FunctionUse Case
Scroll Progress
scroll()
Tied to scroll container position (0-100%)
View Progress
view()
Tied to element visibility in viewport
时间线类型CSS函数适用场景
滚动进度
scroll()
与滚动容器位置绑定(0-100%)
视图进度
view()
与元素在视口中的可见性绑定

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调试

  1. Open DevTools → Elements tab
  2. Find "Scroll-Driven Animations" tab (may be in overflow ››)
  3. Select element with scroll animation
  4. Scrub timeline to preview animation
  5. Inspect animation-timeline and animation-range values
  1. 打开DevTools → 元素标签页
  2. 找到“滚动驱动动画”标签(可能在更多选项 ›› 中)
  3. 选择带有滚动动画的元素
  4. 拖动时间线预览动画
  5. 检查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

浏览器支持

Browserscroll()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

关键决策

DecisionOption AOption BRecommendation
Timeline typescroll()view()view() for reveals, scroll() for progress
Fallback strategyIntersectionObserverNo animationIntersectionObserver fallback
Animation propertiesAll CSStransform/opacitytransform/opacity only
Range unitsPercentagesNamed rangesNamed ranges (entry, cover) for clarity
决策项选项A选项B推荐方案
时间线类型scroll()view()**view()**用于显示动画,**scroll()**用于进度条
降级策略IntersectionObserver无动画IntersectionObserver降级方案
动画属性所有CSS属性transform/opacity仅使用transform/opacity
范围单位百分比命名范围命名范围(entry、cover)更清晰

Related Skills

相关技能

  • motion-animation-patterns
    - Framer Motion for JS animations
  • core-web-vitals
    - Performance impact considerations
  • view-transitions
    - Complementary page transitions
  • motion-animation-patterns
    - 用于JavaScript动画的Framer Motion
  • 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

参考资料

  • references/css-scroll-timeline.md
    - CSS scroll() and view() functions
  • references/js-api.md
    - JavaScript ScrollTimeline/ViewTimeline API
  • scripts/parallax-section.tsx
    - React parallax component
  • references/css-scroll-timeline.md
    - CSS scroll()和view()函数
  • references/js-api.md
    - JavaScript ScrollTimeline/ViewTimeline API
  • scripts/parallax-section.tsx
    - React视差组件