滚动视差效果是一种视觉技术,当用户向下滚动页面时,不同层的内容会以不同的速度移动。这会营造出一种深度错觉,让前景元素看起来比背景元素移动得更快,从而带来引人入胜、身临其境的浏览体验。最好使用 CSS 滚动驱动动画来实现此效果,这样您就可以将动画与容器的滚动位置相关联。
如何实现
下面介绍如何创建基本的视差效果:
创建封装容器元素:此元素只是将视差效果的所有图层组合在一起。它不是可滚动元素,因此其溢出部分应被剪裁。此外,还要为其指定一个与视差效果的其中一个图层的高度相匹配的
height。<div class="wrapper"> … </div>.wrapper { overflow: clip; height: 100vh; /* Height of one of the layers of the parallax */ }声明图层:在封装容器内,添加将以不同速度移动的各个图层。
<div class="wrapper"> <div class="layer">LAYER 0</div> <div class="layer">LAYER 1</div> <div class="layer">LAYER 2</div> … </div>添加平移动画:定义一个 CSS 动画,用于更改图层的
transform属性。对于视差效果,您通常会使用translateY垂直移动图层。@keyframes parallax { from { transform: translateY(700px); } }设置
view-timeline:如需将动画与滚动位置相关联,请在封装容器元素上创建view-timeline,然后将其应用于图层。.wrapper { view-timeline: --wrapper; } .layer { animation: parallax linear both; animation-timeline: --wrapper; }错开动画:如要让图层以不同的速度移动,您可以使用以下两种主要方法之一:在关键帧中错开,或错开
animation-range。这两种方法都可以使用硬编码值,也可以使用
sibling-index()/sibling-count()实现。硬编码值最简单,在只有有限数量的层时也很有用。如果您有许多层,sibling-index()/sibling-count()实现会非常方便。关键帧中的交错:
使用硬编码值,您可以为每个图层定义自定义属性,以手动控制其视差偏移。
.layer:nth-child(1) { --offset: 100px; } .layer:nth-child(2) { --offset: 200px; } .layer:nth-child(3) { --offset: 300px; } @keyframes parallax { from { transform: translateY(var(--offset)); } }使用
sibling-index(),让sibling-index()函数返回子元素在其同级元素中的索引,以自动计算交错效果。@keyframes parallax { from { transform: translateY(calc(100px * sibling-index())); } }错开
animation-range:使用硬编码值,您可以单独为每个层明确定义
animation-range的边界。.layer:nth-child(1) { animation-range: entry 25% exit 50%; } .layer:nth-child(2) { animation-range: entry 25% exit 75%; } .layer:nth-child(3) { animation-range: entry 25% exit 100%; }使用
sibling-index()和sibling-count(),您可以根据总层数 (sibling-count()) 通过数学方法计算范围。.layer { animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index()); }
示例代码
@keyframes parallax {
from {
transform: translateY(calc(100px * sibling-index()));
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
或者,您也可以使用 animation-range 属性来实现类似的效果:
@keyframes parallax {
from {
transform: translateY(700px);
}
}
.wrapper {
view-timeline: --wrapper;
}
.layer {
animation: parallax linear both;
animation-timeline: --wrapper;
animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}
@media (prefers-reduced-motion: reduce) {
.layer {
animation: none;
}
}
最佳做法
使用滚动驱动动画时,请务必遵循一些最佳实践,以确保提供流畅且无障碍的体验:
- 务必添加功能检测:并非所有浏览器都支持滚动驱动的动画。使用
@supports ((animation-timeline: view()) and (animation-range: entry))检查是否支持,并为不支持的浏览器提供后备方案。- 此处必须包含
(animation-range: entry)检查,以滤除仅提供部分支持的浏览器。 - 请勿将
scroll-timeline-polyfill软件包用于回退策略,因为它功能不完整,并且存在许多已知问题。 - 如果动画仅被视为装饰性元素,请选择渐进增强,不要提供后备。
- 此处必须包含
- 务必尊重用户偏好:有些用户更喜欢在网页上看到较少的动画。使用
prefers-reduced-motion媒体查询为这些用户停用或减少动画。 - 务必尝试仅为高性能 CSS 属性添加动画效果:为了获得最流畅的动画效果,请坚持为可由浏览器的合成器线程处理的属性(例如
transform和opacity)添加动画效果。为其他属性(例如width或height)添加动画效果可能会导致性能问题。 - 务必使用正确的声明顺序:使用
animation简写属性时,请在它之后声明animation-timeline和animation-range,以防止简写属性重置时间轴。
至于设置 animation-range:
- 务必为所有层指定相同的起始偏移量,例如
entry 25% - 务必为所有层指定不同的结束偏移量,并使用
sibling-count()和sibling-index()分配偏移量,例如exit calc(100% / sibling-count() * sibling-index())。
浏览器支持和后备策略
因此,通常需要采用后备策略。
对于不支持滚动驱动动画的浏览器,您可以使用后备方案来重现视觉效果。后备方案通常使用滚动监听器(用于 ScrollTimeline 效果)或 IntersectionObserver API(用于 ViewTimeline 效果)构建。
在内置支持滚动驱动动画的浏览器中,请始终使用原生 CSS 实现,因为这些实现的性能更高。
请注意,并非所有效果都可以通过后备方法重现。
具体而言,对于此用例,以下脚本会为不支持滚动驱动动画的浏览器应用回退。它使用 IntersectionObserver 跟踪 .wrapper 元素的可见性,并根据滚动位置更新图层的 transform 属性。
// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
const wrapper = document.querySelector('.wrapper');
const layers = document.querySelectorAll('.layer');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
window.addEventListener('scroll', onScroll);
} else {
window.removeEventListener('scroll', onScroll);
}
});
}, { threshold: 0 });
observer.observe(wrapper);
function onScroll() {
const scrollY = window.scrollY;
const wrapperRect = wrapper.getBoundingClientRect();
const wrapperTop = wrapperRect.top + scrollY;
const wrapperHeight = wrapperRect.height;
const windowHeight = window.innerHeight;
if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
layers.forEach((layer, index) => {
// This matches the effect as defined in the CSS example above.
// Customize this further if needed.
const initialTranslateY = 100 * index;
const translateY = initialTranslateY * (1 - scrollPercent);
layer.style.transform = `translateY(${translateY}px)`;
});
}
}
// Trigger onScroll once to set initial positions
onScroll();
}