This demo has several versions:
scroll-timeline
ScrollTimeline
This demo features three content columns. The central column follows regular scroll, yet the two surrounding ones scroll in the opposite direction.
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.
.columns
.column
.column-reverse
<div class="columns"> <div class="column column-reverse">…</div> <div class="column">…</div> <div class="column column-reverse">…</div> </div>
The basic layout is done using CSS Grid, and inside each .column, the contained .column__item elements are positioned using CSS Flexbox
.column__item
/* Three column layout */ .columns { display: grid; grid-template-columns: repeat(3, 1fr); } .column { display: flex; flex-direction: column; }
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.
-100%
100vh
/* 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.
overflow
/* 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.
-100% + 100vh
100%
/* 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
scroll()
/* 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.