Scrollytelling 是一种用于打造引人入胜、身临其境的 Web 体验的热门技术。它涉及在用户滚动页面时为页面上的元素添加动画效果,从而有效地讲述故事或引导用户浏览叙事。借助 CSS 滚动驱动动画,您可以直接在 CSS 中创建这些效果,而无需依赖 JavaScript。动画由滚动位置控制,而不是由基于时间的时钟控制,这可确保动画始终与用户的滚动同步。
如何实现
若要打造滚动叙事体验,您需要两组元素:一组用于跟踪滚动位置,另一组用于添加动画效果。
首先,在要跟踪的元素上定义一个命名的 view-timeline。这些将作为动画的驱动因素。
#tracked {
section:nth-child(1){ view-timeline: --tl-1 block; }
section:nth-child(2){ view-timeline: --tl-2 block; }
section:nth-child(3){ view-timeline: --tl-3 block; }
section:nth-child(4){ view-timeline: --tl-4 block; }
section:nth-child(5){ view-timeline: --tl-5 block; }
}
接下来,为要添加动画效果的元素应用动画,并使用 animation-timeline 属性将这些元素关联到您刚刚创建的时间轴。
#animated {
section {
animation: animate-in auto linear both, animate-out auto linear forwards;
animation-range: entry 25% cover 50%, exit 50% exit 75%;
}
section:nth-child(1){ animation-timeline: --tl-1; }
section:nth-child(2){ animation-timeline: --tl-2; }
section:nth-child(3){ animation-timeline: --tl-3; }
section:nth-child(4){ animation-timeline: --tl-4; }
section:nth-child(5){ animation-timeline: --tl-5; }
}
为了让 animation-timeline 能够引用具名时间轴,它们需要位于同一作用域内。您可以在共同祖先元素上使用 timeline-scope 属性,使时间轴可供需要它们的所有元素使用。:root 元素通常是实现此目的的理想选择。
html {
timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}
最后,您可以使用 animation-range 属性指定动画应在时间轴中的哪个确切范围内运行。这样,您就可以精细控制动画的触发时间和进度。
#animated section {
animation-range: entry 25% cover 50%, exit 50% exit 75%;
}
示例代码
html {
timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}
#tracked {
section:nth-child(1){ view-timeline: --tl-1 block; }
section:nth-child(2){ view-timeline: --tl-2 block; }
section:nth-child(3){ view-timeline: --tl-3 block; }
section:nth-child(4){ view-timeline: --tl-4 block; }
section:nth-child(5){ view-timeline: --tl-5 block; }
}
@keyframes animate-in {
from { scale: 0.5; opacity: 0; transform: rotateY(-180deg); }
to { transform: rotateY(0deg); }
}
@keyframes animate-out {
to { translate: 100% 0; opacity: 0; }
}
#animated {
section {
animation: animate-in auto linear both, animate-out auto linear forwards;
animation-range: entry 25% cover 50%, exit 50% exit 75%;
backface-visibility: hidden;
}
section:nth-child(1){ animation-timeline: --tl-1; }
section:nth-child(2){ animation-timeline: --tl-2; }
section:nth-child(3){ animation-timeline: --tl-3; }
section:nth-child(4){ animation-timeline: --tl-4; }
section:nth-child(5){ animation-timeline: --tl-5; }
}
/* MANDATORY Copy-Paste Safety: Disable continuous storytelling motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
#animated section {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}
最佳做法
使用滚动驱动动画时,请务必遵循一些最佳实践,以确保提供流畅且无障碍的体验:
- 务必添加功能检测:并非所有浏览器都支持滚动驱动的动画。使用
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%))检查是否支持,并为不支持的浏览器提供后备方案。- 此处必须包含
(animation-range: 0% 100%)检查,以滤除仅提供部分支持的浏览器。 - 请勿将
scroll-timeline-polyfill软件包用于回退策略,因为它功能不完整,并且存在许多已知问题。 - 如果动画仅被视为装饰性元素,请选择渐进增强,不要提供后备。
- 此处必须包含
- 务必尊重用户偏好:有些用户更喜欢在网页上看到较少的动画。使用
prefers-reduced-motion媒体查询为这些用户停用或减少动画。 - 务必尝试仅为高性能 CSS 属性添加动画效果:为了获得最流畅的动画效果,请坚持为可由浏览器的合成器线程处理的属性(例如
transform和opacity)添加动画效果。为其他属性(例如width或height)添加动画效果可能会导致性能问题。 - 务必使用正确的声明顺序:使用
animation简写属性时,请在它之后声明animation-timeline和animation-range,以防止简写属性重置时间轴。
使用 view-timeline 属性创建滚动驱动的动画时:
- 务必使用 CSS
<dashed-ident>作为名称。 - 可选:明确指定要跟踪的轴。如果目标不是默认的
block轴(例如在水平滚动条中),请使用view-timeline-axis明确指定要跟踪的轴。 - 务必确保查找范围有效:如果声明
view-timeline的元素不是动画元素的平面树祖先,请在共享祖先上使用timeline-scope来提升view-timeline名称的可见性。
后备策略
对于不支持滚动驱动动画的浏览器,您可以使用后备方案来重现视觉效果。后备方案通常使用滚动监听器(用于 ScrollTimeline 效果)或 IntersectionObserver API(用于 ViewTimeline 效果)构建。
在内置支持滚动驱动动画的浏览器中,请始终使用原生 CSS 实现,因为这些实现的性能更高。
请注意,并非所有效果都可以通过后备方法重现。
具体而言,对于此用例,以下脚本会为不支持滚动驱动动画的浏览器应用回退。它使用 IntersectionObserver 跟踪每个 #tracked section 元素的可见性,并相应地更新对应 #animated section 的 transform 属性。
const animatedSections = document.querySelectorAll('#animated section');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const sectionIndex = Array.from(document.querySelectorAll('#tracked section')).indexOf(entry.target);
if (sectionIndex !== -1) {
const animatedSection = animatedSections[sectionIndex];
const ratio = entry.intersectionRatio;
// Animate-in
animatedSection.style.opacity = ratio;
animatedSection.style.transform = `scale(${0.5 + ratio * 0.5}) rotateY(${-180 + ratio * 180}deg)`;
// Animate-out
if (ratio < 0.5) {
animatedSection.style.translate = `${(0.5 - ratio) * 2 * 100}% 0`;
} else {
animatedSection.style.translate = '0 0';
}
}
});
}, { threshold: Array.from({length: 101}, (_, i) => i / 100) });
document.querySelectorAll('#tracked section').forEach(section => {
observer.observe(section);
});
以及随附的 CSS:
#animated section {
opacity: 0;
transform: scale(0.5) rotateY(-180deg);
backface-visibility: hidden;
}
/* MANDATORY Copy-Paste Safety: Ensure content remains fully visible and legible for assistive technologies or users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
#animated section {
opacity: 1 !important;
transform: none !important;
translate: 0 0 !important;
}
}
此后备方案可为不支持原生 CSS 功能的浏览器提供更准确的滚动驱动动画,从而确保为所有用户提供更一致的体验。通过为 IntersectionObserver 使用一系列阈值,我们可以更精确地跟踪滚动位置,并创建更流畅的动画。