scrollytelling

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Scrollytelling Skill

滚动叙事(Scrollytelling)技能

Build scroll-driven narrative experiences that reveal content, trigger animations, and create immersive storytelling as users scroll.
构建由滚动触发的叙事体验,用户滚动时会展示内容、触发动画,打造沉浸式叙事效果。

What is Scrollytelling?

什么是滚动叙事(Scrollytelling)?

Definition: "A storytelling format in which visual and textual elements appear or change as the reader scrolls through an online article." When readers scroll, something other than conventional document movement happens.
Origin: The New York Times' "Snow Fall: The Avalanche at Tunnel Creek" (2012), which won the 2013 Pulitzer Prize for Feature Writing.
Why it works: Scrollytelling exploits a fundamental psychological principle—humans crave control. Every scroll is a micro-commitment that increases engagement. Users control the pace, creating deeper connection than passive consumption.
Measured impact:
  • 400% longer time-on-page vs static content
  • 67% improvement in information recall
  • 5x higher social sharing rates
  • 25-40% improved conversion completion
定义:一种叙事形式,读者滚动在线文章时,视觉和文本元素会随之出现或变化。读者滚动时,会发生常规文档滚动之外的其他交互。
起源:《纽约时报》2012年推出的《Snow Fall: The Avalanche at Tunnel Creek》,该作品获得了2013年普利策特稿写作奖。
为何有效:滚动叙事利用了人类渴望掌控感这一基本心理原则。每一次滚动都是一次微小的投入,能提升用户参与度。用户可以控制节奏,相比被动消费,能建立更深的情感连接。
已验证的效果
  • 相比静态内容,页面停留时长提升400%
  • 信息回忆率提升67%
  • 社交分享率提升5倍
  • 转化完成率提升25-40%

Core Principles

核心原则

1. Story First, Technology Second

1. 叙事优先,技术为辅

The biggest mistake is leading with technology instead of narrative. Scrollytelling should enhance the story, not showcase effects.
最大的误区是先考虑技术而非叙事。滚动叙事应服务于故事,而非单纯展示特效。

2. User Agency & Progressive Disclosure

2. 用户自主性与渐进式披露

Users control the pace. Information reveals gradually to maintain curiosity. This shifts from predetermined pacing to user-controlled narrative flow.
用户控制节奏。信息逐步展示,维持好奇心。这将叙事节奏从预设模式转变为用户主导模式。

3. Sequential Structure

3. 线性结构

Unlike hierarchical web content, scrollytelling demands linear progression with clear narrative beats. Each section builds on the previous.
与层级化的网页内容不同,滚动叙事需要清晰的线性叙事节奏,每个部分都建立在前一部分的基础上。

4. Meaningful Change

4. 有意义的变化

Every scroll-triggered effect must serve the narrative. Gratuitous animation distracts rather than enhances.
每一个由滚动触发的效果都必须服务于叙事。无意义的动画只会分散注意力,而非增强体验。

5. Restraint Over Spectacle

5. 克制胜于炫技

Not every section needs animation. Subtle transitions often work better than constant effects. The format should amplify the content's message, not fight it.
并非每个部分都需要动画。微妙的过渡往往比持续的特效效果更好。这种形式应放大内容的核心信息,而非与之冲突。

The 5 Standard Techniques

5种标准实现技巧

Research analyzing 50 scrollytelling articles identified these core patterns:
TechniqueDescriptionBest For
Graphic SequenceDiscrete visuals that change completely at scroll thresholdsData visualizations, step-by-step explanations
Animated TransitionSmooth morphing between statesState changes, evolution over time
Pan and ZoomScroll controls which portion of a visual is visibleMaps, large images, spatial narratives
MoviescrollerFrame-by-frame progression creating video-like effectsProduct showcases, 3D object reveals
Show-and-PlayInteractive elements activate at scroll waypointsMultimedia, audio/video integration
通过分析50篇滚动叙事文章,总结出以下核心模式:
技巧描述适用场景
图形序列在滚动阈值处,视觉内容完全切换数据可视化、分步讲解
动画过渡状态间平滑变形状态变化、随时间演变的内容
平移与缩放通过滚动控制可见的视觉区域地图、大尺寸图片、空间叙事
逐帧滚动动画逐帧推进,创造类视频效果产品展示、3D物体揭秘
展示与交互滚动到特定节点时激活交互元素多媒体、音视频集成

Layout Patterns

布局模式

Pattern 1: Side-by-Side Sticky (Most Common)

模式1:并排固定布局(最常见)

The classic scrollytelling pattern: a graphic becomes "stuck" while narrative text scrolls alongside. When the narrative concludes, the graphic "unsticks."
┌─────────────────────────────────────┐
│  ┌──────────┐  ┌─────────────────┐  │
│  │  Text    │  │                 │  │
│  │  Step 1  │  │    STICKY       │  │
│  ├──────────┤  │    GRAPHIC      │  │
│  │  Text    │  │                 │  │
│  │  Step 2  │  │  (updates with  │  │
│  ├──────────┤  │   active step)  │  │
│  │  Text    │  │                 │  │
│  │  Step 3  │  │                 │  │
│  └──────────┘  └─────────────────┘  │
└─────────────────────────────────────┘
When to use: Data visualization stories, step-by-step explanations, educational content requiring persistent visual context.
Implementation: Use CSS
position: sticky
(not JavaScript scroll listeners) for better performance and graceful degradation.
经典的滚动叙事模式:图形内容“固定”在一侧,叙事文本在另一侧滚动。叙事结束后,图形解除固定。
┌─────────────────────────────────────┐
│  ┌──────────┐  ┌─────────────────┐  │
│  │  Text    │  │                 │  │
│  │  Step 1  │  │    STICKY       │  │
│  ├──────────┤  │    GRAPHIC      │  │
│  │  Text    │  │                 │  │
│  │  Step 2  │  │  (updates with  │  │
│  ├──────────┤  │   active step)  │  │
│  │  Text    │  │                 │  │
│  │  Step 3  │  │                 │  │
│  └──────────┘  └─────────────────┘  │
└─────────────────────────────────────┘
适用场景:数据可视化故事、分步讲解、需要持续视觉上下文的教育内容。
实现方式:使用CSS
position: sticky
(而非JavaScript滚动监听),以获得更好的性能和优雅降级效果。

Pattern 2: Full-Width Sections

模式2:全屏区块布局

Content spans the entire viewport with section-based transitions.
When to use: Highly visual narratives, immersive brand storytelling, portfolio showcases, timeline-based stories.
内容铺满整个视口,基于区块实现过渡效果。
适用场景:高视觉化叙事、沉浸式品牌故事、作品集展示、时间线类故事。

Pattern 3: Layered Parallax

模式3:分层视差

Multiple visual layers (background, midground, foreground) move at different speeds to create depth.
When to use: Atmospheric storytelling, game/product launches, long-form narratives where depth adds emotional impact.
Accessibility warning: Parallax triggers vestibular disorders (dizziness, nausea, migraines). Always provide reduced-motion fallback; limit to one subtle parallax effect per page maximum.
多个视觉层(背景、中景、前景)以不同速度移动,营造深度感。
适用场景:氛围感叙事、游戏/产品发布会、需要深度增强情感冲击的长篇叙事。
无障碍警告:视差效果可能引发前庭功能障碍(头晕、恶心、偏头痛)。务必提供减少动效的回退方案;每页最多只使用一种微妙的视差效果。

Pattern 4: Multi-Directional

模式4:多方向滚动

Combines vertical scrolling with horizontal sections or sideways timelines.
When to use: Timeline-based content, visually-driven showcases, unconventional layouts where surprise enhances the message.
结合垂直滚动与水平区块或横向时间线。
适用场景:时间线类内容、视觉驱动的展示、非常规布局(惊喜感能强化信息传递)。

When to Use Scrollytelling

何时使用滚动叙事

Good candidates:
  • Long-form journalism with multimedia
  • Brand storytelling celebrating achievements
  • Product pages showcasing features
  • Chronological/historical content
  • Complex narratives broken into digestible chunks
  • High-consideration products needing depth
Avoid when:
  • You lack strong visual assets
  • You're tight on time/budget (good scrollytelling requires more investment)
  • The story lacks distinct chronology
  • Content is brief
  • Performance is critical on low-end devices
适合场景
  • 带有多媒体的长篇新闻报道
  • 品牌成就叙事
  • 展示产品特性的产品页
  • chronological/历史类内容
  • 拆分为易理解模块的复杂叙事
  • 需要深度讲解的高决策成本产品
避免场景
  • 缺乏优质视觉素材
  • 时间/预算紧张(优质的滚动叙事需要更多投入)
  • 故事缺乏清晰的时间线
  • 内容简短
  • 低端设备上的性能要求极高

Discovery Questions

需求确认问题

Before implementing, clarify with the user:
header: "Scrollytelling Pattern"
question: "What scrollytelling pattern fits your narrative?"
options:
  - "Pinned narrative - text changes while visual stays fixed (NYT, Pudding.cool style)"
  - "Progressive reveal - content fades in as you scroll down"
  - "Parallax depth - layers move at different speeds (requires reduced-motion fallback)"
  - "Step sequence - discrete sections with transitions between"
  - "Hybrid - multiple patterns combined"
header: "Tech Stack"
question: "What's your frontend setup?"
options:
  - "React + Tailwind"
  - "React + CSS-in-JS"
  - "Next.js"
  - "Vue"
  - "Vanilla JS"
  - "Other"
header: "Animation Approach"
question: "Animation library preference?"
options:
  - "CSS-only (scroll-timeline API, IntersectionObserver) - best performance"
  - "GSAP ScrollTrigger - most powerful, cross-browser"
  - "Framer Motion / Motion - React ecosystem"
  - "Lenis + custom - smooth scroll"
  - "No preference - recommend based on complexity"
实施前,需与用户确认以下内容:
header: "滚动叙事模式"
question: "哪种滚动叙事模式适合你的故事?"
options:
  - "固定叙事 - 文本变化,视觉内容固定(NYT、Pudding.cool风格)"
  - "渐进式展示 - 滚动时内容渐入"
  - "视差深度 - 图层以不同速度移动(需提供减少动效回退方案)"
  - "分步序列 - 独立区块间带过渡效果"
  - "混合模式 - 结合多种模式"
header: "技术栈"
question: "你的前端技术栈是什么?"
options:
  - "React + Tailwind"
  - "React + CSS-in-JS"
  - "Next.js"
  - "Vue"
  - "原生JS"
  - "其他"
header: "动画方案"
question: "偏好的动画库?"
options:
  - "纯CSS(scroll-timeline API、IntersectionObserver)- 性能最佳"
  - "GSAP ScrollTrigger - 功能最强大,跨浏览器兼容"
  - "Framer Motion / Motion - React生态"
  - "Lenis + 自定义 - 平滑滚动"
  - "无偏好 - 根据复杂度推荐"

Technical Implementation (2025-2026)

技术实现(2025-2026)

Technology Selection Guide

技术选型指南

ComplexityRecommendationBundle Size
Simple reveals, progress barsNative CSS scroll-timeline0 KB
Viewport-triggered effectsIntersectionObserver0 KB
Complex timelines, pinningGSAP ScrollTrigger~23 KB
React projectsMotion (Framer Motion)~32 KB
Smooth scroll + effectsLenis + GSAP~25 KB
复杂度推荐方案包体积
简单展示、进度条原生CSS scroll-timeline0 KB
视口触发效果IntersectionObserver0 KB
复杂时间线、固定区块GSAP ScrollTrigger~23 KB
React项目Motion(Framer Motion)~32 KB
平滑滚动+效果Lenis + GSAP~25 KB

CSS Scroll-Driven Animations (Native - 2025+)

CSS滚动驱动动画(原生 - 2025+)

Browser support:
  • Chrome 115+: Full support (since July 2025)
  • Safari 26+: Full support (since September 2025)
  • Firefox: Requires flag (
    layout.css.scroll-driven-animations.enabled
    )
Key properties:
  • animation-timeline: scroll()
    - links animation to scroll position
  • animation-timeline: view()
    - links animation to element visibility
  • animation-range
    - controls when animation starts/stops
Example - View-triggered fade in:
css
@supports (animation-timeline: scroll()) {
  .reveal-on-scroll {
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }

  @keyframes reveal {
    from {
      opacity: 0;
      transform: translateY(30px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
}
Example - Scroll-linked progress bar:
css
.progress-bar {
  animation: grow linear;
  animation-timeline: scroll();
}

@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
Performance benefit: Tokopedia achieved 80% code reduction and CPU usage dropped from 50% to 2% by switching to native CSS scroll-driven animations.
浏览器支持
  • Chrome 115+:完全支持(2025年7月起)
  • Safari 26+:完全支持(2025年9月起)
  • Firefox:需开启实验性标志(
    layout.css.scroll-driven-animations.enabled
核心属性
  • animation-timeline: scroll()
    - 将动画与滚动位置关联
  • animation-timeline: view()
    - 将动画与元素可见性关联
  • animation-range
    - 控制动画的开始/结束时机
示例 - 视口触发渐入:
css
@supports (animation-timeline: scroll()) {
  .reveal-on-scroll {
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }

  @keyframes reveal {
    from {
      opacity: 0;
      transform: translateY(30px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
}
示例 - 滚动关联进度条:
css
.progress-bar {
  animation: grow linear;
  animation-timeline: scroll();
}

@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
性能收益:Tokopedia通过切换到原生CSS滚动驱动动画,实现了80%的代码量减少,CPU使用率从50%降至2%。

IntersectionObserver Pattern

IntersectionObserver模式

For scroll-triggered effects without continuous scroll tracking:
tsx
const RevealOnScroll = ({ children, delay = 0 }) => {
  const ref = useRef(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    // Check reduced motion preference
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setIsVisible(true);
      return;
    }

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1, rootMargin: '-50px' }
    );

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div
      ref={ref}
      style={{
        opacity: isVisible ? 1 : 0,
        transform: isVisible ? 'translateY(0)' : 'translateY(30px)',
        transition: `all 0.6s ease ${delay}ms`,
      }}
    >
      {children}
    </div>
  );
};
适用于无需持续跟踪滚动的触发式效果:
tsx
const RevealOnScroll = ({ children, delay = 0 }) => {
  const ref = useRef(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    // 检查减少动效偏好
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setIsVisible(true);
      return;
    }

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1, rootMargin: '-50px' }
    );

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div
      ref={ref}
      style={{
        opacity: isVisible ? 1 : 0,
        transform: isVisible ? 'translateY(0)' : 'translateY(30px)',
        transition: `all 0.6s ease ${delay}ms`,
      }}
    >
      {children}
    </div>
  );
};

GSAP ScrollTrigger (Complex Animations)

GSAP ScrollTrigger(复杂动画)

For pinned sections, timeline orchestration, and cross-browser reliability:
tsx
const ScrollytellingSection = ({ steps }) => {
  const containerRef = useRef(null);
  const [activeStep, setActiveStep] = useState(0);

  useEffect(() => {
    // Check reduced motion preference
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      return;
    }

    const ctx = gsap.context(() => {
      steps.forEach((_, index) => {
        ScrollTrigger.create({
          trigger: `.step-${index}`,
          start: 'top center',
          end: 'bottom center',
          onEnter: () => setActiveStep(index),
          onEnterBack: () => setActiveStep(index),
        });
      });
    }, containerRef);

    return () => ctx.revert();
  }, [steps]);

  return (
    <section ref={containerRef} className="relative">
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
        {/* Text column - scrolls naturally */}
        <div className="space-y-[100vh]">
          {steps.map((step, i) => (
            <div
              key={i}
              className={`step-${i} min-h-screen flex items-center transition-opacity duration-300 ${
                activeStep === i ? 'opacity-100' : 'opacity-30'
              }`}
            >
              <div className="max-w-md">
                <h3 className="text-2xl font-bold mb-4">{step.title}</h3>
                <p className="text-lg">{step.description}</p>
              </div>
            </div>
          ))}
        </div>

        {/* Visual column - sticky */}
        <div className="relative hidden md:block">
          <div className="sticky top-0 h-screen flex items-center justify-center">
            <StepVisual step={activeStep} data={steps[activeStep]} />
          </div>
        </div>
      </div>
    </section>
  );
};
适用于固定区块、时间线编排和跨浏览器兼容场景:
tsx
const ScrollytellingSection = ({ steps }) => {
  const containerRef = useRef(null);
  const [activeStep, setActiveStep] = useState(0);

  useEffect(() => {
    // 检查减少动效偏好
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      return;
    }

    const ctx = gsap.context(() => {
      steps.forEach((_, index) => {
        ScrollTrigger.create({
          trigger: `.step-${index}`,
          start: 'top center',
          end: 'bottom center',
          onEnter: () => setActiveStep(index),
          onEnterBack: () => setActiveStep(index),
        });
      });
    }, containerRef);

    return () => ctx.revert();
  }, [steps]);

  return (
    <section ref={containerRef} className="relative">
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
        {/* 文本列 - 自然滚动 */}
        <div className="space-y-[100vh]">
          {steps.map((step, i) => (
            <div
              key={i}
              className={`step-${i} min-h-screen flex items-center transition-opacity duration-300 ${
                activeStep === i ? 'opacity-100' : 'opacity-30'
              }`}
            >
              <div className="max-w-md">
                <h3 className="text-2xl font-bold mb-4">{step.title}</h3>
                <p className="text-lg">{step.description}</p>
              </div>
            </div>
          ))}
        </div>

        {/* 视觉列 - 固定 */}
        <div className="relative hidden md:block">
          <div className="sticky top-0 h-screen flex items-center justify-center">
            <StepVisual step={activeStep} data={steps[activeStep]} />
          </div>
        </div>
      </div>
    </section>
  );
};

Motion (Framer Motion) for React

Motion(Framer Motion)for React

tsx
import { motion, useScroll, useTransform } from 'motion/react';

const ScrollLinkedSection = () => {
  const ref = useRef(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start end', 'end start'],
  });

  const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
  const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8]);

  return (
    <section ref={ref} className="h-[200vh] relative">
      <motion.div
        className="sticky top-0 h-screen flex items-center justify-center"
        style={{ opacity, scale }}
      >
        <h2 className="text-6xl font-bold">Scroll-Linked Content</h2>
      </motion.div>
    </section>
  );
};
tsx
import { motion, useScroll, useTransform } from 'motion/react';

const ScrollLinkedSection = () => {
  const ref = useRef(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start end', 'end start'],
  });

  const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
  const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8]);

  return (
    <section ref={ref} className="h-[200vh] relative">
      <motion.div
        className="sticky top-0 h-screen flex items-center justify-center"
        style={{ opacity, scale }}
      >
        <h2 className="text-6xl font-bold">Scroll-Linked Content</h2>
      </motion.div>
    </section>
  );
};

Scroll Progress Hook

滚动进度Hook

tsx
const useScrollProgress = (ref) => {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const updateProgress = () => {
      const rect = element.getBoundingClientRect();
      const windowHeight = window.innerHeight;

      // 0 when element top enters viewport, 1 when bottom exits
      const start = rect.top - windowHeight;
      const end = rect.bottom;
      const current = -start;
      const total = end - start;

      setProgress(Math.max(0, Math.min(1, current / total)));
    };

    window.addEventListener('scroll', updateProgress, { passive: true });
    updateProgress();
    return () => window.removeEventListener('scroll', updateProgress);
  }, [ref]);

  return progress;
};
tsx
const useScrollProgress = (ref) => {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const updateProgress = () => {
      const rect = element.getBoundingClientRect();
      const windowHeight = window.innerHeight;

      // 元素顶部进入视口时为0,底部离开时为1
      const start = rect.top - windowHeight;
      const end = rect.bottom;
      const current = -start;
      const total = end - start;

      setProgress(Math.max(0, Math.min(1, current / total)));
    };

    window.addEventListener('scroll', updateProgress, { passive: true });
    updateProgress();
    return () => window.removeEventListener('scroll', updateProgress);
  }, [ref]);

  return progress;
};

Accessibility Requirements

无障碍要求

Accessibility is non-negotiable. Scrollytelling can trigger vestibular disorders and exclude keyboard/screen reader users if not implemented correctly.
无障碍是必不可少的。如果实现不当,滚动叙事可能引发前庭功能障碍,还会排斥键盘/屏幕阅读器用户。

Critical WCAG Criteria

关键WCAG标准

CriterionRequirement
SC 2.2.2Auto-playing content >5 seconds needs pause/stop/hide controls
SC 2.3.3User-triggered animations must be disableable
SC 2.3.1No flashing more than 3 times per second
标准要求
SC 2.2.2自动播放超过5秒的内容需提供暂停/停止/隐藏控件
SC 2.3.3用户触发的动画必须可关闭
SC 2.3.1闪烁频率不超过每秒3次

Reduced Motion Support (Mandatory)

减少动效支持(强制要求)

Always respect user preferences:
css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
    scroll-behavior: auto !important;
  }
}
tsx
const prefersReducedMotion = () =>
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// In components - check before animating
useEffect(() => {
  if (prefersReducedMotion()) {
    // Show content immediately, skip animations
    return;
  }
  // Set up animations
}, []);
务必尊重用户偏好:
css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
    scroll-behavior: auto !important;
  }
}
tsx
const prefersReducedMotion = () =>
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// 组件中 - 动画前先检查
useEffect(() => {
  if (prefersReducedMotion()) {
    // 立即显示内容,跳过动画
    return;
  }
  // 设置动画
}, []);

Safe Animation Guidelines

安全动画指南

Animation TypeSafety
Fade in/out, under 0.5sSafe
Simple transformsSafe
Parallax scrollingTriggers vestibular issues - always provide fallback
Swooping, zoomingProblematic - avoid or provide fallback
Looping animationsCognitive overload - limit iterations
Keep effects small: Animations affecting more than 1/3 of the viewport can overwhelm users.
动画类型安全性
渐入/渐出,时长<0.5s安全
简单变换安全
视差滚动可能引发前庭问题 - 务必提供回退方案
快速移动、缩放有问题 - 避免或提供回退方案
循环动画认知过载 - 限制循环次数
保持效果简洁:影响超过1/3视口的动画会使用户不堪重负。

Keyboard Navigation

键盘导航

  • Add
    tabindex="0"
    to scrollable areas
  • Ensure focus follows scroll targets when using smooth scrolling
  • Provide skip links to major sections
  • No keyboard traps in scroll regions
  • 为可滚动区域添加
    tabindex="0"
  • 使用平滑滚动时,确保焦点跟随滚动目标
  • 提供跳转到主要区块的快捷链接
  • 滚动区域中不能出现键盘陷阱

Screen Reader Considerations

屏幕阅读器适配

  • Use proper heading hierarchy (
    <h1>
    through
    <h6>
    )
  • DOM order must match logical reading order
  • Use ARIA live regions for dynamic content updates
  • Ensure all content exists in DOM (even if visually hidden initially)
  • 使用正确的标题层级(
    <h1>
    <h6>
  • DOM顺序必须符合逻辑阅读顺序
  • 动态内容更新使用ARIA实时区域
  • 确保所有内容都存在于DOM中(即使初始时视觉隐藏)

Performance Best Practices

性能最佳实践

Do

建议做法

  • Use
    transform
    and
    opacity
    for animations (GPU-accelerated)
  • Add
    will-change: transform
    sparingly on animated elements
  • Use
    passive: true
    on scroll listeners
  • Use
    position: sticky
    over JS-based pinning
  • Lazy load images/videos until needed
  • Use single IntersectionObserver for multiple elements
  • Test on real devices, not just desktop
  • 使用
    transform
    opacity
    实现动画(GPU加速)
  • 仅在即将动画的元素上少量使用
    will-change: transform
  • 滚动监听使用
    passive: true
  • 使用
    position: sticky
    替代JS实现的固定效果
  • 按需懒加载图片/视频
  • 多个元素使用同一个IntersectionObserver
  • 在真实设备上测试,而非仅在桌面端

Don't

避免做法

  • Animate
    width
    ,
    height
    ,
    top
    ,
    left
    (triggers layout recalculation)
  • Use
    will-change
    excessively (increases memory usage)
  • Create scroll listeners without cleanup
  • Forget to handle reduced-motion preferences
  • Use
    overflow: hidden
    on ancestors of sticky elements
  • 动画
    width
    height
    top
    left
    (会触发布局重排)
  • 过度使用
    will-change
    (增加内存占用)
  • 创建滚动监听器但不清理
  • 忽略减少动效偏好设置
  • 在固定元素的祖先节点上使用
    overflow: hidden

Performance Targets

性能目标

  • First Contentful Paint: under 2.5 seconds
  • Maintain 60fps during scrolling
  • Meet Core Web Vitals thresholds
  • 首次内容绘制:2.5秒以内
  • 滚动时保持60fps
  • 达到Core Web Vitals阈值

Mobile Considerations

移动端注意事项

Mobile users represent 60%+ of web traffic. Scrollytelling must work excellently on mobile or risk excluding the majority of users.
移动端用户占网络流量的60%以上。滚动叙事必须在移动端表现出色,否则会排斥大部分用户。

Mobile-First Design Philosophy

移动端优先设计理念

Start with mobile: "Starting with mobile first forces you to pare down your experience to the nuts and bolts, leaving only the necessities. This refines and focuses the content." (The Pudding)
Design the core experience for mobile, then enhance for desktop—not the reverse. This approach:
  • Forces essential-only content decisions
  • Improves development efficiency
  • Results in less code if desktop is functionally similar
从移动端开始:“从移动端开始设计,会迫使你将体验精简到核心内容,只保留必要部分。这能优化并聚焦内容。”(来自The Pudding)
先为移动端设计核心体验,再为桌面端增强——而非反过来。这种方法:
  • 迫使你做出只保留核心内容的决策
  • 提升开发效率
  • 如果桌面端功能与移动端相似,代码量会更少

Viewport Units: vh vs svh vs dvh vs lvh

视口单位:vh vs svh vs dvh vs lvh

Mobile browsers toggle navigation bars during scrolling, breaking traditional
100vh
layouts.
UnitDefinitionWhen To Use
vh
Large viewport (browser UI hidden)Legacy fallback only
svh
Small viewport (browser UI visible)Use for ~90% of layouts (recommended)
lvh
Large viewport (browser UI hidden)Modals/overlays maximizing space
dvh
Dynamic viewport (changes constantly)Use sparingly - causes layout thrashing
Critical warning: "I initially thought 'dynamic viewport units are the future' and used dvh for every element. This was a mistake. The constant layout shifts felt broken."
Implementation pattern:
css
.full-height-section {
  height: 100vh;  /* Fallback for older browsers */
  height: 100svh; /* Modern solution - small viewport */
}

/* Progressive enhancement */
@supports (height: 100svh) {
  :root {
    --viewport-height: 100svh;
  }
}
JavaScript alternative (The Pudding's recommendation):
javascript
function setViewportHeight() {
  const vh = window.innerHeight;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
}

window.addEventListener('resize', setViewportHeight);
setViewportHeight();
css
.section {
  height: calc(var(--vh) * 100);
}
移动浏览器在滚动时会切换导航栏,破坏传统的
100vh
布局。
单位定义适用场景
vh
大视口(浏览器UI隐藏)仅作为遗留回退方案
svh
小视口(浏览器UI可见)90%的布局推荐使用(推荐)
lvh
大视口(浏览器UI隐藏)最大化空间的模态框/覆盖层
dvh
动态视口(持续变化)谨慎使用 - 会引发布局抖动
重要警告:“我最初认为‘动态视口单位是未来’,并在所有元素上使用dvh。这是个错误。持续的布局偏移会让页面感觉有问题。”
实现模式
css
.full-height-section {
  height: 100vh;  /* 旧浏览器回退 */
  height: 100svh; /* 现代解决方案 - 小视口 */
}

/* 渐进增强 */
@supports (height: 100svh) {
  :root {
    --viewport-height: 100svh;
  }
}
JavaScript替代方案(The Pudding推荐):
javascript
function setViewportHeight() {
  const vh = window.innerHeight;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
}

window.addEventListener('resize', setViewportHeight);
setViewportHeight();
css
.section {
  height: calc(var(--vh) * 100);
}

Touch Scroll Physics

触摸滚动物理特性

Momentum scrolling: Content continues scrolling after touch release, decelerating naturally. iOS and Android have different friction curves—iOS feels more "flicky."
Critical for iOS:
css
.scroll-container {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch; /* iOS momentum - still needed for pre-iOS 13 */
}
Performance note: Scroll events fire at END of momentum on iOS, not during. Use IntersectionObserver instead of scroll listeners for step detection.
动量滚动:触摸释放后内容继续滚动,自然减速。iOS和Android的摩擦曲线不同——iOS感觉更“灵敏”。
iOS关键设置
css
.scroll-container {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch; /* iOS动量滚动 - iOS 13之前仍需此前缀 */
}
性能说明:iOS上滚动事件在动量滚动结束时触发,而非过程中。使用IntersectionObserver替代滚动监听器来检测步骤。

Preventing Gesture Conflicts

避免手势冲突

Pull-to-Refresh conflicts:
css
/* Disable PTR but keep bounce effects */
html {
  overscroll-behavior-y: contain;
}

/* Or disable completely */
html {
  overscroll-behavior-y: none;
}
Scroll chaining in modals:
css
.modal-content {
  overflow-y: auto;
  overscroll-behavior: contain; /* Prevents scrolling parent when modal hits boundary */
}
Horizontal swipe conflicts (browser back/forward):
css
.horizontal-carousel {
  touch-action: pan-y pinch-zoom; /* Allow vertical scroll & zoom, block horizontal */
}
下拉刷新冲突
css
/* 禁用下拉刷新但保留弹性效果 */
html {
  overscroll-behavior-y: contain;
}

/* 或完全禁用 */
html {
  overscroll-behavior-y: none;
}
模态框滚动链
css
.modal-content {
  overflow-y: auto;
  overscroll-behavior: contain; /* 模态框滚动到边界时,防止父元素滚动 */
}
水平滑动冲突(浏览器前进/后退):
css
.horizontal-carousel {
  touch-action: pan-y pinch-zoom; /* 允许垂直滚动和缩放,阻止水平滑动 */
}

Passive Event Listeners

被动事件监听器

Chrome 56+ defaults touch listeners to passive for 60fps scrolling. Use passive listeners for monitoring, non-passive only when you must
preventDefault()
:
javascript
// ✅ Monitoring scroll (passive - default, best performance)
document.addEventListener('touchstart', trackTouch, { passive: true });

// ⚠️ Only when you MUST prevent default (e.g., custom swipe)
carousel.addEventListener('touchmove', handleSwipe, { passive: false });
Prefer CSS over JavaScript:
css
/* Better than JavaScript preventDefault */
.element {
  touch-action: pan-y pinch-zoom;
}
Chrome 56+默认将触摸监听器设为被动,以实现60fps滚动。仅在必须调用
preventDefault()
时使用非被动监听器,其他场景使用被动监听器:
javascript
// ✅ 监控滚动(被动 - 默认,性能最佳)
document.addEventListener('touchstart', trackTouch, { passive: true });

// ⚠️ 仅在必须阻止默认行为时使用(如自定义滑动)
carousel.addEventListener('touchmove', handleSwipe, { passive: false });
优先使用CSS而非JavaScript
css
/* 比JavaScript的preventDefault更好 */
.element {
  touch-action: pan-y pinch-zoom;
}

Scroll Snap on Mobile

移动端滚动吸附

Scroll snap works well on mobile with
mandatory
(avoid
proximity
on touch devices):
css
.scroll-container {
  scroll-snap-type: y mandatory;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.section {
  scroll-snap-align: start;
  min-height: 100svh;
}

/* Accessibility */
@media (prefers-reduced-motion: reduce) {
  .scroll-container {
    scroll-snap-type: none;
    scroll-behavior: auto;
  }
}
Warning: Never use
mandatory
if content can overflow the viewport—users won't be able to scroll to see it.
移动端滚动吸附使用
mandatory
效果更佳(触摸设备避免使用
proximity
):
css
.scroll-container {
  scroll-snap-type: y mandatory;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.section {
  scroll-snap-align: start;
  min-height: 100svh;
}

/* 无障碍适配 */
@media (prefers-reduced-motion: reduce) {
  .scroll-container {
    scroll-snap-type: none;
    scroll-behavior: auto;
  }
}
警告:如果内容可能超出视口,绝不要使用
mandatory
——用户将无法滚动查看超出部分。

Touch Accessibility

触摸无障碍

Minimum touch target sizes:
StandardSizeWhen
WCAG 2.5.8 (AA)24×24pxMinimum compliance
WCAG 2.5.5 (AAA)44×44pxBest practice
Apple iOS44×44ptRecommended
Android48×48dpRecommended
Expand touch area without changing visual size:
css
.small-button {
  width: 24px;
  height: 24px;
  padding: 10px; /* Creates 44×44px touch target */
}
Always provide button alternatives for gesture-only actions:
html
<!-- Swipe to delete MUST have button alternative -->
<div class="item">
  <span>Content</span>
  <button aria-label="Delete">Delete</button>
</div>
最小触摸目标尺寸
标准尺寸适用场景
WCAG 2.5.8 (AA)24×24px最低合规要求
WCAG 2.5.5 (AAA)44×44px最佳实践
Apple iOS44×44pt推荐
Android48×48dp推荐
不改变视觉尺寸的前提下扩大触摸区域
css
.small-button {
  width: 24px;
  height: 24px;
  padding: 10px; /* 创造44×44px的触摸目标 */
}
所有纯手势操作必须提供按钮替代方案
html
<!-- 滑动删除必须有按钮替代 -->
<div class="item">
  <span>Content</span>
  <button aria-label="Delete">Delete</button>
</div>

Browser-Specific Quirks

浏览器特有问题

iOS Safari:
css
/* Position sticky requires no overflow on ancestors */
.parent {
  /* overflow: hidden; ❌ Breaks sticky on iOS */
}

.sticky {
  position: -webkit-sticky; /* Prefix still needed */
  position: sticky;
  top: 0;
}
css
/* Preventing body scroll in modals requires JavaScript on iOS */
/* CSS overflow: hidden doesn't work on body */
javascript
// iOS modal scroll lock
function lockScroll() {
  document.body.style.position = 'fixed';
  document.body.style.top = `-${window.scrollY}px`;
}
Chrome Android:
css
/* Disable pull-to-refresh */
body {
  overscroll-behavior-y: none;
}
iOS Safari
css
/* position sticky要求祖先节点不能有overflow */
.parent {
  /* overflow: hidden; ❌ 在iOS上会破坏固定效果 */
}

.sticky {
  position: -webkit-sticky; /* 仍需前缀 */
  position: sticky;
  top: 0;
}
css
/* iOS上模态框中锁定body滚动需要JavaScript */
/* CSS的overflow: hidden在body上无效 */
javascript
// iOS模态框滚动锁定
function lockScroll() {
  document.body.style.position = 'fixed';
  document.body.style.top = `-${window.scrollY}px`;
}
Chrome Android
css
/* 禁用下拉刷新 */
body {
  overscroll-behavior-y: none;
}

Responsive Layout Strategy

响应式布局策略

Side-by-Side → Stacked pattern:
tsx
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
  {/* On mobile: stacked, full-width */}
  {/* On desktop: side-by-side sticky */}
  <div className="space-y-[50vh] md:space-y-[100vh]">
    {/* Text steps - shorter spacing on mobile */}
  </div>
  <div className="hidden md:block">
    {/* Sticky visual - hidden on mobile, shown on desktop */}
  </div>
</div>
Synchronize CSS and JS breakpoints:
javascript
const breakpoint = '(min-width: 800px)';
const isDesktop = window.matchMedia(breakpoint).matches;

if (isDesktop) {
  initScrollama(); // Complex scrollytelling
} else {
  initStackedView(); // Simple stacked layout
}

// Listen for breakpoint changes
window.matchMedia(breakpoint).addEventListener('change', (e) => {
  if (e.matches) {
    initScrollama();
  } else {
    initStackedView();
  }
});
Mobile alternative patterns:
  • Replace sticky graphics with inline graphics between text sections
  • Use simpler reveal animations instead of complex parallax
  • Stack static images with scroll-triggered captions
  • Consider whether scrollytelling is even appropriate
并排→堆叠模式
tsx
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
  {/* 移动端:堆叠,全屏宽 */}
  {/* 桌面端:并排固定 */}
  <div className="space-y-[50vh] md:space-y-[100vh]">
    {/* 文本步骤 - 移动端间距更小 */}
  </div>
  <div className="hidden md:block">
    {/* 固定视觉内容 - 移动端隐藏,桌面端显示 */}
  </div>
</div>
同步CSS和JS断点
javascript
const breakpoint = '(min-width: 800px)';
const isDesktop = window.matchMedia(breakpoint).matches;

if (isDesktop) {
  initScrollama(); // 复杂滚动叙事
} else {
  initStackedView(); // 简单堆叠布局
}

// 监听断点变化
window.matchMedia(breakpoint).addEventListener('change', (e) => {
  if (e.matches) {
    initScrollama();
  } else {
    initStackedView();
  }
});
移动端替代模式
  • 用文本区块间的内联图形替代固定图形
  • 用简单的渐入动画替代复杂视差
  • 堆叠静态图片,滚动时触发标题显示
  • 考虑滚动叙事是否适合移动端

Mobile Performance Strategies

移动端性能策略

Target: 60fps (16.7ms per frame)
Use hardware-accelerated properties only:
css
.animate {
  /* ✅ Good - GPU accelerated */
  transform: translateY(100px);
  opacity: 0.5;

  /* ❌ Bad - triggers layout recalculation */
  /* top: 100px; width: 200px; margin: 20px; */
}
Use
will-change
sparingly
:
css
/* Only on elements about to animate */
.about-to-animate {
  will-change: transform;
}

/* Remove when animation completes */
.animation-complete {
  will-change: auto;
}
Warning: Too many composited layers hurt mobile performance. Don't apply
will-change
to everything.
Throttle scroll handlers with requestAnimationFrame:
javascript
let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateAnimation();
      ticking = false;
    });
    ticking = true;
  }
}, { passive: true });
Better: Use IntersectionObserver (no scroll events):
javascript
const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      animateElement(entry.target);
    }
  });
});
目标:60fps(每帧16.7ms)
仅使用硬件加速属性
css
.animate {
  /* ✅ 良好 - GPU加速 */
  transform: translateY(100px);
  opacity: 0.5;

  /* ❌ 糟糕 - 触发布局重排 */
  /* top: 100px; width: 200px; margin: 20px; */
}
谨慎使用
will-change
css
/* 仅在即将动画的元素上使用 */
.about-to-animate {
  will-change: transform;
}

/* 动画完成后移除 */
.animation-complete {
  will-change: auto;
}
警告:过多的合成层会影响移动端性能。不要给所有元素都加
will-change
用requestAnimationFrame节流滚动处理
javascript
let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateAnimation();
      ticking = false;
    });
    ticking = true;
  }
}, { passive: true });
更好的方案:使用IntersectionObserver(无需滚动事件):
javascript
const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      animateElement(entry.target);
    }
  });
});

When to Simplify or Abandon Scrollytelling on Mobile

何时在移动端简化或放弃滚动叙事

Keep scrollytelling when:
  • Transitions are truly meaningful to the narrative
  • Spatial movement or temporal change is core to understanding
  • Performance targets can be met (60fps, <3s load)
  • Testing shows mobile users successfully comprehend content
Simplify scrollytelling when:
  • Performance is acceptable but animations aren't essential
  • Some complexity is nice-to-have but not required
  • Desktop experience is richer but mobile should be functional
Abandon scrollytelling when:
  • Performance issues can't be resolved on mid-tier devices
  • Mobile users are confused or frustrated in testing
  • Development timeline doesn't allow proper optimization
  • Content works just as well in simpler stacked format
  • Animations are decorative, not meaningful
"The most important reason to preserve scroll animations is if the transitions are truly meaningful, not just something to make it pop." (The Pudding)
保留滚动叙事的场景
  • 过渡效果对叙事至关重要
  • 空间移动或时间变化是理解内容的核心
  • 能达到性能目标(60fps,加载<3s)
  • 测试显示移动端用户能理解内容
简化滚动叙事的场景
  • 性能可接受,但动画非必需
  • 部分复杂度是锦上添花,非必须
  • 桌面端体验更丰富,但移动端需保证功能可用
放弃滚动叙事的场景
  • 中端设备上的性能问题无法解决
  • 测试中移动端用户感到困惑或沮丧
  • 开发时间不允许充分优化
  • 内容用简单堆叠布局同样效果好
  • 动画仅为装饰,无实际意义
“保留滚动动画的最重要原因是过渡效果对叙事真正有意义,而非仅仅为了让页面更炫酷。”(来自The Pudding)

Mobile Testing Checklist

移动端测试清单

Device Coverage:
  • iPhone (latest 2 models) - Safari
  • iPhone - Chrome
  • iPad - Safari (portrait & landscape)
  • Android flagship - Chrome
  • Android mid-tier - Chrome (critical for performance)
  • Tablet Android - Chrome
Viewport Testing:
  • Address bar hide/show transitions smooth
  • No layout jumping during scroll
  • Fixed elements stay positioned correctly
  • svh/dvh units behaving as expected
Performance Testing:
  • 60fps maintained during scroll (use Chrome DevTools FPS meter)
  • No janky animations
  • Images lazy-load properly
  • Memory doesn't leak on long sessions
  • Test with CPU throttling (4x slowdown in DevTools)
Interaction Testing:
  • Touch scrolling feels natural (momentum)
  • No accidental interactions
  • Pull-to-refresh disabled if needed
  • Scroll chaining behaves correctly
Accessibility Testing:
  • Works with reduced motion enabled
  • Touch targets minimum 44×44px
  • Button alternatives for all gestures
  • Screen reader (VoiceOver/TalkBack) can navigate
Critical: Chrome DevTools mobile emulator does NOT accurately simulate browser UI behavior. Test on real devices or use BrowserStack/Sauce Labs
设备覆盖
  • iPhone(最新2款)- Safari
  • iPhone - Chrome
  • iPad - Safari(竖屏&横屏)
  • Android旗舰 - Chrome
  • Android中端 - Chrome(性能测试关键)
  • Android平板 - Chrome
视口测试
  • 地址栏显示/隐藏过渡平滑
  • 滚动时无布局抖动
  • 固定元素位置正确
  • svh/dvh单位表现符合预期
性能测试
  • 滚动时保持60fps(使用Chrome DevTools FPS计量器)
  • 无卡顿动画
  • 图片懒加载正常
  • 长时间使用无内存泄漏
  • CPU节流测试(DevTools中4倍减速)
交互测试
  • 触摸滚动自然(带动量)
  • 无意外交互
  • 按需禁用下拉刷新
  • 滚动链行为正确
无障碍测试
  • 开启减少动效后正常工作
  • 触摸目标最小44×44px
  • 所有手势都有按钮替代
  • 屏幕阅读器(VoiceOver/TalkBack)可导航
重要提示:Chrome DevTools移动端模拟器无法准确模拟浏览器UI行为。务必在真实设备上测试,或使用BrowserStack/Sauce Labs。

Anti-Patterns to Avoid

需避免的反模式

Anti-PatternProblemSolution
Scroll-jackingOverrides natural scroll, breaks accessibilityPreserve native scroll behavior
Text-graphics conflictUser can't read text while watching animationSeparate text and animated areas
Animation overloadDistracts from content, causes fatigueRestraint—not every section needs effects
No length indicatorUsers don't know commitment levelAdd progress indicators
Missing fallbacksBreaks for no-JS or reduced-motion usersProgressive enhancement
Mobile neglectExcludes 60% of usersMobile-first design
Poor pacingToo fast or too slow content revealsTest with real users
反模式问题解决方案
滚动劫持覆盖原生滚动,破坏无障碍保留原生滚动行为
文本与图形冲突用户无法同时阅读文本和观看动画分离文本和动画区域
动画过载分散内容注意力,导致疲劳克制——并非每个部分都需要效果
无长度指示器用户不知道需要投入多少时间添加进度指示器
无回退方案在无JS或开启减少动效的环境中失效渐进增强
忽略移动端排斥60%的用户移动端优先设计
节奏不当内容展示过快或过慢真实用户测试

Implementation Workflow

实施流程

  1. Understand the narrative - What story are you telling? What's the sequence?
  2. Choose pattern - Pinned, progressive, parallax, or hybrid?
  3. Plan accessibility - How will reduced-motion users experience this?
  4. Select technology - Native CSS, GSAP, Motion based on complexity
  5. Scaffold structure - Build HTML/component structure first
  6. Add scroll mechanics - Implement tracking (IntersectionObserver, ScrollTrigger, etc.)
  7. Wire animations - Connect scroll state to visual changes
  8. Add reduced-motion fallbacks - Content should work without animation
  9. Performance audit - Check for jank, optimize
  10. Cross-device testing - Mobile, tablet, desktop, different browsers
  11. Accessibility testing - Keyboard nav, screen readers, reduced motion
  1. 理解叙事 - 你要讲什么故事?叙事顺序是什么?
  2. 选择模式 - 固定、渐进、视差还是混合模式?
  3. 规划无障碍 - 开启减少动效的用户如何体验?
  4. 选择技术 - 根据复杂度选择原生CSS、GSAP、Motion
  5. 搭建结构 - 先构建HTML/组件结构
  6. 添加滚动机制 - 实现跟踪(IntersectionObserver、ScrollTrigger等)
  7. 关联动画 - 将滚动状态与视觉变化关联
  8. 添加减少动效回退 - 内容在无动画时也能正常使用
  9. 性能审计 - 检查卡顿,优化性能
  10. 跨设备测试 - 移动端、平板、桌面端、不同浏览器
  11. 无障碍测试 - 键盘导航、屏幕阅读器、减少动效

Output

输出成果

After gathering requirements, implement the scrollytelling experience directly in the codebase. Provide:
  1. Component structure with scroll tracking
  2. Animation/transition logic with reduced-motion handling
  3. Responsive adjustments (mobile-first)
  4. Accessible fallbacks
  5. Performance optimizations
  6. Testing recommendations
收集需求后,直接在代码库中实现滚动叙事体验。需提供:
  1. 带滚动跟踪的组件结构
  2. 带减少动效处理的动画/过渡逻辑
  3. 响应式调整(移动端优先)
  4. 无障碍回退方案
  5. 性能优化
  6. 测试建议

Notable Examples for Reference

参考案例

  • NYT "Snow Fall" - Origin story, multimedia integration
  • Pudding.cool - Data journalism with audio-visual sync
  • National Geographic "Atlas of Moons" - Educational, responsive design
  • BBC "Partition of India" - Historical narrative with multimedia
  • Firewatch website - Multi-layered parallax for atmosphere
  • NYT《Snow Fall》 - 起源之作,多媒体集成
  • Pudding.cool - 数据新闻,音视频同步
  • 国家地理《Atlas of Moons》 - 教育类,响应式设计
  • BBC《Partition of India》 - 历史叙事,多媒体
  • Firewatch官网 - 多层视差,营造氛围

Sources

资料来源

Research based on 100+ sources including EU Data Visualization Guide, MDN, GSAP documentation, W3C WCAG, A List Apart, Smashing Magazine, The Pudding, CSS-Tricks, and academic research on scrollytelling effectiveness.
基于100+资料研究,包括欧盟数据可视化指南、MDN、GSAP文档、W3C WCAG、A List Apart、Smashing Magazine、The Pudding、CSS-Tricks,以及滚动叙事效果的学术研究。