🚨 UPDATE 2023.05.08 The contents of this post are outdated, as it uses an old syntax of an earlier version of the Scroll-Driven Animations Specification.
For a demo that uses up-to-date code, please refer to https://scroll-driven-animations.style/demos/reverse-scroll/css/.
The other day I saw “Alternate Column Scroll” by Manoela Ilic float by. It’s a tutorial/demo in which some of the content columns scroll in the opposite direction, powered by Locomotive Scroll.
Based on Manoela’s demo I recreated the scrolling part with CSS @scroll-timeline
.
~
Table of Contents
~
# The Basic Structure
I based myself upon Manoela’s original markup and start with 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>
</div>
The basic layout is done using CSS Grid:
/* Three column layout */
.columns {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
Inside each .column
, the contained .column__item
elements are positioned using CSS Flexbox:
.column {
display: flex;
flex-direction: column;
}
💡 We choose flexbox here because we’ll change flex-direction
for the .column-reverse
columns later on.
Combined, the starter template looks like this:
See the Pen Alternate Column Scroll (CSS @scroll-timeline first + JS ScrollTimeline fallback + Polyfill) by Bramus (@bramus) on CodePen.
~
# Implementing Reverse Scrolling using CSS @scroll-timeline
# Adjusting the columns
In browsers that support CSS @scroll-timeline
we shift the .column-reverse
columns up by translating them by -100%
. As they are now entirely off-screen, we counteract that by adding 100vh
. 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));
}
As we don’t want .columns
to grow by that translation, we also prevent the content from bleeding out of it.
/* 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, we also reverse the order for the items inside each column. 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;
}
💡 To detect whether a browser supports CSS @scroll-timeline
we use @supports
and feature check on the telltale animation-timeline
CSS Property.
/* Scroll-Timeline Supported, Yay! */
@supports (animation-timeline: works) {
…
}
Browsers that support @scroll-timeline
also support animation-timeline
.
~
# Setting up + Attaching the Animation and Scroll-Timeline
Our ScrollTimeline is the default Scroll-Timeline of scrolling through the document from top to bottom. To set it up we don’t need anything special.
/* Set up scroll-timeline */
@scroll-timeline scroll-in-document {
source: auto; /* Default scroll-timeline: scrolling in the document */
}
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));
}
}
☝️ I know, you can omit the from
here, but I’m including it here for educational purposes.
Using the animation-timeline we finally link up our ScrollTimeline to the .column-reverse
elements:
/* Hook our animation with the timeline to our columns */
.column-reverse {
animation: 1s adjust-position linear forwards;
animation-timeline: scroll-in-document;
}
💭 Curious to learn more about CSS @scroll-timeline
? Go read this article here on bram.us covering @scroll-timeline
in more detail.
~
# All together now
Combined, our demo now looks like this:
See the Pen Reverse-Scrolling Columns with CSS Scroll-Timelinel (+ JS ScrollTimeline Polyfill Fallback) by Bramus (@bramus) on CodePen.
👨🔬 The demo above will only work in Chromium 89+ with the #experimental-web-platform-features
flag enabled through chrome://flags
. That’s because it’s the only browser that supports CSS @scroll-timeline
at the time of writing.
~
# Implementing Reverse Scrolling with JS, for browsers that don’t speak CSS @scroll-timeline
# The Polyfill
In browsers that don’t support CSS @scroll-timeline
, the Scroll-Timeline Polyfill by Robert Flack is loaded and used.
// Polyfill for browsers with no Scroll-Timeline support
import "https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js";
The polyfill will register itself when needed.
🤔 Polyfill?
A polyfill is a piece of code (or plugin) that provides the technology that you, the developer, expect the browser to provide natively — What is a Polyfill?
~
# WAAPI + ScrollTimeline
The animation itself is a regular WAAPI animation and does exactly the same as the CSS Animation described above. The animation is, however, extended with a linked ScrollTimeline
instance the polyfill provides us.
Above that, the entire code is wrapped inside a little snippet that feature detects CSS support for animation-timeline
. When it’s not supported, the contained code will be executed.
// Fallback for browsers that don't support CSS ScrollTimeline
if (!CSS.supports("animation-timeline: foo")) {
// As we're about to shift content out of .columns, we need it to hide its overflow
document.querySelector(".columns").style.overflowY = "hidden";
// Set up ScrollTimeline instance
const timeline = new ScrollTimeline({
scrollSource: document.documentElement,
timeRange: 1,
fill: "both"
});
// Loop all eligible columns
document.querySelectorAll(".column-reverse").forEach(($column) => {
// Flip item order in reverse columns
$column.style.flexDirection = "column-reverse";
// Hook Animation
$column.animate(
{
transform: [
"translateY(calc(-100% + 100vh))",
"translateY(calc(100% - 100vh))"
]
},
{
duration: 1,
fill: "both",
timeline
}
);
});
}
💭 Curious to learn more about WAAPI + ScrollTimeline? Go read this article on CSS-Tricks that covers WAAPI + ScrollTimeline in more detail.
~
# All together now
With that, our final demo now becomes this:
See the Pen Reverse-Scrolling Columns with CSS Scroll-Timelinel (+ JS ScrollTimeline Polyfill Fallback) by Bramus (@bramus) on CodePen.
I know, there’s a FOUC here. I’ll leave it to you, reader, as an exercise to solve.
~
# What about the rest?
The rest of the demo has not been recreated, as that falls out of the scope of CSS @scroll-timeline
. To recreate those, keep an eye out on the Shared Element Transitions API that allows a simple set of transitions in both Single-Page Applications (SPAs) and Multi-Page Applications (MPAs).
~
🔥 Like what you see? Want to stay in the loop? Here's how:
This is great!
But in Safari it is very laggy and the images sometimes flicker. Do you have an Idea why this is the case?
At the moment, Safari does not support Scroll-Driven Animations, so a polyfill is used to drive the anations. That polyfill is written in JavaScript and, unlike the CSS animations, is blocking the main thread. This explains why it is laggy (and why CSS Scroll-Driven Animations are an awesome feature).
Do note that the contents of this post are outdated, as it uses an old syntax of an earlier version of the Scroll-Driven Animations Specification.
For a demo that uses up-to-date code, please refer to https://scroll-driven-animations.style/demos/reverse-scroll/css/
I have added a warning banner at the top of this page to include this updated info.