Version selector

This demo has several versions:

  1. CSS scroll-timeline
  2. JavaScript WAAPI + ScrollTimeline

About this demo

This demo features three content columns. The central column follows regular scroll, yet the two surrounding ones scroll in the opposite direction.

The Code / Explanation

Base layout

As for the basic layout, there is one .columns wrapper containing three nested .column elements. The columns that should reverse scroll, get the class .column-reverse added as well.

<div class="columns">
	<div class="column column-reverse">…</div>
	<div class="column">…</div>
	<div class="column column-reverse">…</div>

The basic layout is done using CSS Grid, and inside each .column, the contained .column__item elements are positioned using CSS Flexbox

/* Three column layout */
.columns {
	display: grid;
	grid-template-columns: repeat(3, 1fr);

.column {
	display: flex;
	flex-direction: column;

Scroll-driven animations

In browsers that support CSS Scroll-driven animations, the .column-reverse columns are shifted up by translating them by -100%. As they are now entirely off-screen, add 100vh to pull them back in view. That way their bottom edge touches the bottom edge of the viewport.

/* Shift entire column up, but not so much that it goes out of view */
.column-reverse {
	transform: translateY(calc(-100% + 100vh));

To prevent the wrapping .columns from growing by that translation, prevent the content from bleeding out of it using the overflow property.

/* As we're about to shift content out of .columns, we need it to hide its overflow */
.columns {
	overflow-y: hidden;

Finally, as the columns will be scrolling in a reversed direction, the order for the items inside each column needs to be reversed as well. That way the first item will be at the bottom of the reversed column.

/* Flip item order in reversed columns */
.column-reverse {
	flex-direction: column-reverse;

The Animation itself starts at the translation we’ve already done (-100% + 100vh). The end position is a translation in the opposite direction: 100% down, but minus 100vh to keep it on-screen.

/* Set up Animation */
@keyframes adjust-position {
	/* Start position: shift entire column up, but not so that it goes out of view */
	from {
		transform: translateY(calc(-100% + 100vh));

	/* End position: shift entire column down, but not so that it goes out of view */
	to {
		transform: translateY(calc(100% - 100vh));

The animation is attached to each reversed column, tracking the root scroller in the block direction. For this, the scroll() function is used, creating an anonymous Scroll-Timeline

/* Hook our animation with the timeline to our columns */
.column-reverse {
	animation: adjust-position linear forwards;
	animation-timeline: scroll(root block);

⚠️ Your browser does not support Scroll-driven Animations. Please use Chrome 115 or newer.

Cyber Blue 2011
Gnostic Will 2012
French Kiss 2013
Half Life 2014
Love Boat 2015
Golden Ray 2016
Blame Game 2017
Lone Dust 2018
Lucky Wood 2019
Good Earth 2020
Empty Words 2021
Nonage Line 2009
Blue Hell 2010
Cold Blood 2011
Tulip Heat 2012
Red Wrath 2013
Bold Human 2014
Loyal Royal 2015
Lone Cone 2016
Dutch Green 2017
Valley Hill 2018
Kale Hale 2019
Fake Cake 2020
Book Belly 2021