Reverse-Scrolling Columns with CSS Scroll-Timeline

🚨 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.

~

~

# 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.

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation

3 Comments

  1. 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?

    1. 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.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.