paid-only post

The Scroll-Driven CSS Animation I’m Most Proud Of (After 4 Rewrites)

I built a scroll-driven CSS animation that took four full rewrites before it stopped fighting me. This is the build log, including the ugly parts and the final clean setup.
The Scroll-Driven CSS Animation I’m Most Proud Of (After 4 Rewrites)
Photo by Becca Tapert / Unsplash

The animation that would not behave

I thought this would be a weekend project. A nice scroll-driven animation for a case study page. A little storytelling while you scroll. Some panels, some motion, nothing crazy.

It took four full rewrites before I stopped hating it.

The funny part is that the final version is dead simple. Almost boring. Everything before that was clever and fragile. So this is the build log of how I got there.

What I actually wanted to build

The page was a timeline of a product build. I wanted a vertical layout where each section filled the viewport. As you scroll, a mockup on the right should animate through states. Think of it as a poor man’s scrollytelling.

Roughly:

  • Left side: text sections stacked vertically.
  • Right side: a sticky viewport mockup that changes state based on scroll position.
  • No hard jumps. Smooth transitions between states.

It felt like the perfect excuse to lean on modern CSS. I wanted as little JavaScript as possible. Maybe none.

Attempt 1: The pure JavaScript control freak

My first instinct was the old script-kid reflex. Grab scroll position, map it to a progress value, feed that into everything.

I started with something like this:

const sections = [...document.querySelectorAll('[data-step]')];
const viewport = document.querySelector('.viewport');

function onScroll() {
  const scrollY = window.scrollY;
  const docHeight = document.body.scrollHeight - window.innerHeight;
  const progress = scrollY / docHeight;

  viewport.style.setProperty('--progress', progress);

  let currentIndex = 0;
  sections.forEach((section, index) => {
    const rect = section.getBoundingClientRect();
    if (rect.top < window.innerHeight * 0.5) {
      currentIndex = index;
    }
  });

  viewport.dataset.step = currentIndex;
}

window.addEventListener('scroll', onScroll);
onScroll();

Then I wired everything in CSS using custom properties and attribute selectors.

This post is for paying subscribers only

Subscribe to continue reading