CSS 原生视差滚动效果
CSS-Native Parallax Effect

原始链接: https://dan-webnotes.com/posts/2026-06-02-css-native-parallax-effect/

现代 CSS 现在提供了一种原生的、高性能的方式来创建视差效果,即使用**滚动驱动动画时间轴(Scroll-driven animation timelines)**,从而无需依赖繁重的 JavaScript 滚动监听器。 通过利用 `view-timeline`,开发者可以直接将元素的动画与它在视口中的进度关联起来。这种实现方式使用简单的声明式类,其中一个 `--parallax-offset` 变量同时控制位移(运动)和缩放。缩放至关重要,因为它能确保元素在运动过程中足够大以覆盖容器背景,从而避免出现空隙。 **主要优势包括:** * **性能:** 动画在主线程之外运行,运动更流畅。 * **简洁:** 整个效果可以通过一个带有易于调整设置的通用类来实现。 * **可访问性:** 通过 `@media (prefers-reduced-motion: reduce)` 禁用效果,可以轻松尊重用户的设置。 实现时,需在容器上定义 `view-timeline-name`,并将子元素的 `animation-timeline` 绑定到该名称。请记住在 `animation` 简写属性之后声明 `animation-timeline`,以确保它不会被覆盖。这种原生方案为传统的基于 JavaScript 的视差库提供了一种简洁、易于维护的替代方案。

抱歉。
相关文章

原文

jun 02, 2026

Parallax effects have a long history, and while there are countless ways and libraries to achieve them, a new CSS-native way was recently made possible with CSS Scroll-driven animation timelines.

The usual recipe was a scroll event listener in JavaScript, recalculating positions on every frame and nudging an element up and down.

Scroll-driven animations handle all of that with CSS. Handling parallax animations with CSS has a few advantages: performance should be better as it runs it off the main thread, but my favorite part is the simplicity with which the whole thing becomes a small block of declarative styles, that can be applied with a single utility class. Here is the full code for the class:

.parallax {
	view-timeline-name: --parallax-tl;
	view-timeline-axis: block;
	overflow: hidden;

	& > * {
		scale: calc(1 + var(--parallax-offset, 20) * 2 / 100);
		animation: parallax auto linear both;
		animation-timeline: --parallax-tl;
		animation-range: cover;
		will-change: translate;
	}
}

@keyframes parallax {
	from {
		translate: 0 calc(var(--parallax-offset, 20) * -1%);
	}
	to {
		translate: 0 calc(var(--parallax-offset, 20) * 1%);
	}
}

The timeline

The trick is view-timeline-name. It creates a view progress timeline, a timeline whose progress is measured by how far the .parallax element has travelled through the scrollport. It reads 0% the moment the element starts to enter the viewport and 100% once it has fully left. view-timeline-axis: block tells it to track movement along the block axis, which is the vertical one in a normal writing mode.

On the child, animation-timeline: --parallax-tl swaps the animation's clock from time to that timeline. From there the rest of the animation line falls into place:

  • auto for duration, because the duration now comes from the timeline rather than a number of seconds
  • linear so scroll progress maps straight onto movement,
  • both to hold the start and end frames outside the active range

⚠️ Note: The animation-timeline longhand property is not part of the animation shorthand and must be declared separately. Furthermore, animation-timeline must be declared after the animation shorthand as the shorthand will reset non-included longhands to their initial value.

The keyframes do the actual work. With the default offset, the child slides from translate: 0 -20% to translate: 0 20% as you scroll past it. Because it moves at a different rate to the container around it, you get that sense of depth.

Scaling to avoid empty spots

The child translates by up to the offset percentage of its own height in each direction, so if the child were exactly the same size as its container, shifting it up or down would expose a strip of empty space.

The child needs to be scaled up in order to have a margin to move into. It needs the offset's worth of extra height above and below, so twice the offset overall:

scale: calc(1 + var(--parallax-offset, 20) * 2 / 100);

With the default offset of 20, the child is rendered at 140% of its size, the surplus is clipped by overflow: hidden on the container, and there is always enough content to cover the box no matter where in the ±20% travel it sits.

The neat part is that both the translate and the scale read the same --parallax-offset variable. Turn the offset up for a stronger effect and the scale grows to match it, so the cover stays correct on its own. One value to tune, and the gaps never come back:

<div class="parallax" style="--parallax-offset: 30;">
	<img src="…" />
</div>

will-change: translate is the last piece, a hint that this element's translate is about to change so the browser can promote it to its own layer ahead of time.

Motion preferences

Parallax is movement tied to scrolling, and some people would rather not have it. It's good practice to respect that by disabling the animation for anyone with prefers-reduced-motion: reduce. In this case, we can just turn off the animation and scale:

@media (prefers-reduced-motion: reduce) {
	.parallax > * {
		animation: none;
		scale: 1;
	}
}

Resources

联系我们 contact @ memedata.com