mcpField
S
scrollytelling

scrollytelling

github.com/doodledood设计与 UI38382 次安装8 周前更新
代码仓库

关于 scrollytelling

面向网页创作者的设计技能,用于规划滚动叙事体验,涵盖叙事模式、实现方式、无障碍设计和性能建议。

主要功能

  1. 01五种滚动触发视觉手法
  2. 02四类页面布局模式
  3. 03CSS、IntersectionObserver、GSAP 和 Motion 实现建议
  4. 04减少动态效果与无障碍指导
  5. 05性能和移动端建议

适用场景

  1. 01规划多媒体长篇报道
  2. 02为产品叙事选择布局
  3. 03为网页添加无障碍滚动效果

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/doodledood/claude-code-plugins --skill scrollytelling

安装后在 Claude Code 中输入/scrollytelling确认已加载。

详细介绍

滚动叙事技能

构建由滚动驱动的叙事体验,在用户滚动时逐步揭示内容、触发动画,并营造沉浸式叙事。

什么是滚动叙事?

定义:“一种叙事形式,读者在滚动浏览在线文章时,视觉和文本元素会随之出现或变化。”当读者滚动时,发生的事情不同于常规的文档移动。

起源:《纽约时报》的《雪崩:隧道溪的雪崩》(2012 年),该作品荣获 2013 年普利策特稿写作奖。

为何有效:滚动叙事利用了一个基本的心理学原理——人类渴望掌控感。每一次滚动都是一次微小的承诺,从而提升参与度。用户掌控节奏,比被动消费建立更深层的连接。

实测影响:

  • 页面停留时间比静态内容长 400%
  • 信息回忆率提升 67%
  • 社交分享率提高 5 倍
  • 转化完成率提升 25-40%

核心原则

1. 故事优先,技术其次

最大的错误是以技术而非叙事为先导。滚动叙事应当增强故事,而非展示特效。

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

用户掌控节奏。信息逐步揭示以保持好奇心。这将预定节奏转变为用户掌控的叙事流。

3. 顺序结构

与层级化的网页内容不同,滚动叙事要求线性推进,并具有清晰的叙事节拍。每个部分都建立在前一部分之上。

4. 有意义的变化

每一个由滚动触发的效果都必须服务于叙事。无谓的动画只会分散注意力,而非增强效果。

5. 克制胜于炫技

并非每个部分都需要动画。微妙的过渡往往比持续的特效更有效。这种形式应当放大内容的信息,而非与之对抗。

5 种标准技法

一项分析 50 篇滚动叙事文章的研究识别出以下核心模式:

技法描述最适合
图形序列在滚动阈值处完全变化的离散视觉元素数据可视化、分步说明
动画过渡状态之间的平滑变形状态变化、随时间演变
平移与缩放滚动控制视觉元素中可见的部分地图、大图、空间叙事
电影滚动逐帧推进,营造类似视频的效果产品展示、3D 物体揭示
展示与播放交互元素在滚动路标处激活多媒体、音频/视频集成

布局模式

模式 1:并排粘性(最常见)

经典的滚动叙事模式:图形“粘住”不动,而叙事文本在旁滚动。当叙事结束时,图形“解除粘住”。

┌─────────────────────────────────────┐
│  ┌──────────┐  ┌─────────────────┐  │
│  │  Text    │  │                 │  │
│  │  Step 1  │  │    STICKY       │  │
│  ├──────────┤  │    GRAPHIC      │  │
│  │  Text    │  │                 │  │
│  │  Step 2  │  │  (updates with  │  │
│  ├──────────┤  │   active step)  │  │
│  │  Text    │  │                 │  │
│  │  Step 3  │  │                 │  │
│  └──────────┘  └─────────────────┘  │
└─────────────────────────────────────┘

何时使用:数据可视化故事、分步说明、需要持续视觉背景的教育内容。

实现:使用 CSS position: sticky(而非 JavaScript 滚动监听器),以获得更好的性能和优雅降级。

模式 2:全宽分节

内容横跨整个视口,并具有基于分节的过渡。

何时使用:高度视觉化的叙事、沉浸式品牌叙事、作品集展示、基于时间线的故事。

模式 3:分层视差

多个视觉层(背景、中景、前景)以不同速度移动以营造深度感。

何时使用:氛围叙事、游戏/产品发布、深度能增强情感冲击的长篇叙事。

无障碍警告:视差会触发前庭功能障碍(头晕、恶心、偏头痛)。务必提供减少动态效果的降级方案;每页最多限制一个微妙的视差效果。

模式 4:多方向

将垂直滚动与水平区块或侧向时间线相结合。

适用场景:基于时间线的内​​容、视觉驱动的展示、通过惊喜感强化信息的非常规布局。

何时使用滚动叙事

适合的场景:

  • 包含多媒体的长篇新闻报道
  • 庆祝成就的品牌故事
  • 展示功能的产品页面
  • 按时间顺序/历史类内容
  • 拆解为易消化片段的复杂叙事
  • 需要深度了解的高考虑度产品

避免使用的情况:

  • 缺乏强有力的视觉素材
  • 时间/预算紧张(优秀的滚动叙事需要更多投入)
  • 故事缺乏明确的时间顺序
  • 内容简短
  • 在低端设备上性能至关重要

需求探索问题

在实施之前,请与用户确认:

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"

技术实现(2025-2026)

技术选型指南
复杂度推荐方案包体积
简单揭示效果、进度条原生 CSS scroll-timeline0 KB
视口触发效果IntersectionObserver0 KB
复杂时间线、固定元素GSAP ScrollTrigger~23 KB
React 项目Motion (Framer Motion)~32 KB
平滑滚动 + 效果Lenis + GSAP~25 KB
CSS 滚动驱动动画(原生 - 2025+)

浏览器支持:

  • Chrome 115+:完全支持(自 2025 年 7 月起)
  • Safari 26+:完全支持(自 2025 年 9 月起)
  • Firefox:需要开启标志(layout.css.scroll-driven-animations.enabled)

关键属性:

  • animation-timeline: scroll() - 将动画与滚动位置关联
  • animation-timeline: view() - 将动画与元素可见性关联
  • animation-range - 控制动画的开始/结束时机

示例 - 视口触发的淡入:

@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);
    }
  }
}

示例 - 滚动关联的进度条:

.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 模式

适用于无需持续跟踪滚动的滚动触发效果:

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>
  );
};
GSAP ScrollTrigger(复杂动画)

适用于固定区块、时间线编排以及跨浏览器可靠性:

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>
  );
};
适用于 React 的 Motion (Framer Motion)
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>
  );
};
滚动进度 Hook
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;
};

无障碍要求

无障碍是不可妥协的。如果实施不当,滚动叙事可能引发前庭功能障碍,并将键盘/屏幕阅读器用户排除在外。

关键 WCAG 标准
标准要求
SC 2.2.2自动播放内容超过 5 秒需要提供暂停/停止/隐藏控件
SC 2.3.3用户触发的动画必须可以禁用
SC 2.3.1闪烁不得超过每秒 3 次
减少动态效果支持(强制)

始终尊重用户偏好:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
    scroll-behavior: auto !important;
  }
}
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
}, []);
安全动画指南
动画类型安全性
淡入/淡出,0.5 秒以内安全
简单变换安全
视差滚动会引发前庭问题 - 始终提供回退方案
俯冲、缩放有问题 - 避免或提供回退方案
循环动画认知过载 - 限制迭代次数

保持效果轻量:影响超过 1/3 视口的动画可能会让用户不堪重负。

键盘导航
  • 为可滚动区域添加 tabindex="0"
  • 使用平滑滚动时,确保焦点跟随滚动目标
  • 为主要区块提供跳转链接
  • 滚动区域内不得存在键盘陷阱
屏幕阅读器注意事项
  • 使用正确的标题层级(<h1> 到 <h6>)
  • DOM 顺序必须与逻辑阅读顺序一致
  • 使用 ARIA 实时区域处理动态内容更新
  • 确保所有内容都存在于 DOM 中(即使初始视觉上隐藏)

性能最佳实践

应该做
  • 使用 transform 和 opacity 实现动画(GPU 加速)
  • 在动画元素上谨慎添加 will-change: transform
  • 在滚动监听器上使用 passive: true
  • 使用 position: sticky 而非基于 JS 的固定定位
  • 延迟加载图片/视频,直到需要时再加载
  • 为多个元素使用单个 IntersectionObserver
  • 在真实设备上测试,而不仅仅是在桌面端
不应该做
  • 对 width、height、top、left 做动画(会触发布局重算)
  • 过度使用 will-change(会增加内存占用)
  • 创建滚动监听器却不清理
  • 忘记处理减弱动效偏好
  • 在 sticky 元素的祖先元素上使用 overflow: hidden
性能目标
  • 首次内容绘制:2.5 秒以内
  • 滚动过程中保持 60fps
  • 满足 Core Web Vitals 阈值

移动端考量

移动端用户占网络流量的 60% 以上。滚动叙事必须在移动端表现出色,否则可能会将大多数用户排除在外。

移动优先的设计理念

从移动端开始:“从移动端入手会迫使你把体验精简到最核心的部分,只保留必需品。这能提炼并聚焦内容。”(The Pudding)

先为移动端设计核心体验,再为桌面端增强——而不是反过来。这种方法:

  • 迫使你只做必要内容的决策
  • 提高开发效率
  • 如果桌面端功能相似,代码量会更少
视口单位:vh vs svh vs dvh vs lvh

移动端浏览器在滚动时会切换导航栏,破坏传统的 100vh 布局。

单位定义何时使用
vh大视口(浏览器 UI 隐藏)仅作为旧版回退
svh小视口(浏览器 UI 可见)约 90% 的布局使用(推荐)
lvh大视口(浏览器 UI 隐藏)模态框/浮层,用于最大化空间
dvh动态视口(不断变化)谨慎使用——会导致布局抖动

重要警告:“我最初以为‘动态视口单位是未来’,于是每个元素都用 dvh。这是个错误。持续的布局偏移让人感觉像是坏了。”

实现模式:

.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 替代方案(The Pudding 的推荐):

function setViewportHeight() {
  const vh = window.innerHeight;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
}

window.addEventListener('resize', setViewportHeight);
setViewportHeight();
.section {
  height: calc(var(--vh) * 100);
}
触摸滚动物理特性

惯性滚动:触摸释放后内容继续滚动,自然减速。iOS 和 Android 有不同的摩擦曲线——iOS 感觉更“滑”。

对 iOS 至关重要:

.scroll-container {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch; /* iOS momentum - still needed for pre-iOS 13 */
}

性能提示:在 iOS 上,滚动事件在惯性结束时触发,而不是过程中。使用 IntersectionObserver 而非滚动监听器来检测步骤。

防止手势冲突

下拉刷新冲突:

/* Disable PTR but keep bounce effects */
html {
  overscroll-behavior-y: contain;
}

/* Or disable completely */
html {
  overscroll-behavior-y: none;
}

模态框中的滚动链:

.modal-content {
  overflow-y: auto;
  overscroll-behavior: contain; /* Prevents scrolling parent when modal hits boundary */
}

水平滑动冲突(浏览器后退/前进):

.horizontal-carousel {
  touch-action: pan-y pinch-zoom; /* Allow vertical scroll & zoom, block horizontal */
}
被动事件监听器

Chrome 56+ 默认将触摸监听器设为被动,以实现 60fps 滚动。监控用途使用被动监听器,只有在必须 preventDefault() 时才使用非被动:

// ✅ 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 });

优先使用 CSS 而非 JavaScript:

/* Better than JavaScript preventDefault */
.element {
  touch-action: pan-y pinch-zoom;
}
移动端的滚动吸附

滚动吸附在移动端配合 mandatory 效果良好(在触摸设备上避免使用 proximity):

.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;
  }
}

警告:如果内容可能溢出视口,切勿使用 mandatory——用户将无法滚动查看内容。

触摸无障碍

最小触摸目标尺寸:

标准尺寸何时
WCAG 2.5.8 (AA)24×24px最低合规要求
WCAG 2.5.5 (AAA)44×44px最佳实践
Apple iOS44×44pt推荐
Android48×48dp推荐

在不改变视觉尺寸的情况下扩大触摸区域:

.small-button {
  width: 24px;
  height: 24px;
  padding: 10px; /* Creates 44×44px touch target */
}

始终为仅手势操作提供按钮替代方案:


<div class="item">
  <span>Content</span>
  <button aria-label="Delete">Delete</button>
</div>
浏览器特定怪癖

iOS Safari:

/* 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;
}
/* Preventing body scroll in modals requires JavaScript on iOS */
/* CSS overflow: hidden doesn't work on body */
// iOS modal scroll lock
function lockScroll() {
  document.body.style.position = 'fixed';
  document.body.style.top = `-${window.scrollY}px`;
}

Chrome Android:

/* Disable pull-to-refresh */
body {
  overscroll-behavior-y: none;
}
响应式布局策略

并排 → 堆叠模式:

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

同步 CSS 和 JS 断点:

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();
  }
});

移动端替代方案:

  • 将粘性图形替换为文本段落之间的内联图形
  • 使用更简单的显现动画,而不是复杂的视差效果
  • 堆叠静态图片,并配以滚动触发的说明文字
  • 考虑滚动叙事是否真的合适
移动端性能策略

目标:60fps(每帧 16.7ms)

仅使用硬件加速属性:

.animate {
  /* ✅ Good - GPU accelerated */
  transform: translateY(100px);
  opacity: 0.5;

  /* ❌ Bad - triggers layout recalculation */
  /* top: 100px; width: 200px; margin: 20px; */
}

谨慎使用 will-change:

/* Only on elements about to animate */
.about-to-animate {
  will-change: transform;
}

/* Remove when animation completes */
.animation-complete {
  will-change: auto;
}

警告:过多的合成层会损害移动端性能。不要对一切都应用 will-change。

使用 requestAnimationFrame 节流滚动处理程序:

let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateAnimation();
      ticking = false;
    });
    ticking = true;
  }
}, { passive: true });

更好:使用 IntersectionObserver(无需滚动事件):

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      animateElement(entry.target);
    }
  });
});
何时在移动端简化或放弃滚动叙事

在以下情况下保留滚动叙事:

  • 转场对叙事确实有意义
  • 空间移动或时间变化是理解内容的核心
  • 可以达到性能目标(60fps,加载时间 <3s)
  • 测试表明移动端用户能够成功理解内容

在以下情况下简化滚动叙事:

  • 性能可接受,但动画并非必要
  • 某些复杂性是锦上添花,但并非必需
  • 桌面端体验更丰富,但移动端应保持可用

在以下情况下放弃滚动叙事:

  • 在中端设备上无法解决性能问题
  • 移动端用户在测试中感到困惑或沮丧
  • 开发时间线不允许进行适当优化
  • 内容在更简单的堆叠格式下同样有效
  • 动画只是装饰性的,而非有意义的

“保留滚动动画最重要的理由,是转场确实有意义,而不只是为了让页面更抓眼球。”(The Pudding)

移动端测试清单

设备覆盖:

  • 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

应避免的反模式

反模式问题解决方案
滚动劫持覆盖自然滚动,破坏无障碍性保留原生滚动行为
文本与图形冲突用户无法在观看动画的同时阅读文本将文本和动画区域分开
动画过载分散对内容的注意力,造成疲劳克制——并非每个部分都需要效果
没有长度指示用户不知道需要投入多少添加进度指示器
缺少回退方案对无 JS 或减少动态效果的用户会失效渐进增强
忽视移动端排除了 60% 的用户移动优先设计
节奏不佳内容显现太快或太慢与真实用户一起测试

实施工作流

  1. 理解叙事 - 你要讲述什么故事?顺序是什么?
  2. 选择模式 - 固定、渐进、视差还是混合?
  3. 规划无障碍 - 减少动态效果的用户将如何体验?
  4. 选择技术 - 根据复杂度选择原生 CSS、GSAP、Motion
  5. 搭建结构 - 先构建 HTML/组件结构
  6. 添加滚动机制 - 实现跟踪(IntersectionObserver、ScrollTrigger 等)
  7. 连接动画 - 将滚动状态与视觉变化关联
  8. 添加减少动态效果的降级方案 - 内容应在无动画时仍可正常使用
  9. 性能审计 - 检查卡顿并优化
  10. 跨设备测试 - 移动端、平板、桌面端、不同浏览器
  11. 无障碍测试 - 键盘导航、屏幕阅读器、减少动态效果

输出

收集需求后,直接在代码库中实现滚动叙事体验。提供:

  1. 带滚动跟踪的组件结构
  2. 带减少动态效果处理的动画/过渡逻辑
  3. 响应式调整(移动优先)
  4. 无障碍降级方案
  5. 性能优化
  6. 测试建议

值得参考的著名示例

  • NYT "Snow Fall" - 起源故事、多媒体整合
  • Pudding.cool - 带音画同步的数据新闻
  • National Geographic "Atlas of Moons" - 教育性、响应式设计
  • BBC "Partition of India" - 带多媒体的历史叙事
  • Firewatch 网站 - 多层视差营造氛围

来源

研究基于 100+ 个来源,包括 EU Data Visualization Guide、MDN、GSAP 文档、W3C WCAG、A List Apart、Smashing Magazine、The Pudding、CSS-Tricks,以及关于滚动叙事有效性的学术研究。

评论

支持 Markdown