Web-Slinger.css: Like Wow.js But With CSS-y Scroll Animations


We had fun in my previous article exploring the goodness of scrolly animations supported in today’s versions of Chrome and Edge (and behind a feature flag in Firefox for now). Those are by and large referred to as “scroll-driven” animations. However, “scroll triggering” is something the Chrome team is still working on. It refers to the behavior you might have seen in the wild in which a point of no return activates a complete animation like a trap after our hapless scrolling user ventures past a certain point. You can see JavaScript examples of this on the Wow.js homepage which assembles itself in a sequence of animated entrances as you scroll down. There is no current official CSS solution for scroll-triggered animations — but Ryan Mulligan has shown how we can make it work by cleverly combining the animation-timeline property with custom properties and style queries.

That is a very cool way to combine new CSS features. But I am not done being overly demanding toward the awesome emergent animation timeline technology I didn’t know existed before I read up on it last month. I noticed scroll timelines and view timelines are geared toward animations that play backward when you scroll back up, unlike the Wow.js example where the dogs roll in and then stay. Bramus mentions the same point in his exploration of scroll-triggered animations. The animations run in reverse when scrolling back up. This is not always feasible. As a divorced Dad, I can attest that the Tinder UI is another example of a pattern in which scrolling and swiping can have irreversible consequences.

Scroll till the cows come home with Web-Slinger.css

Believe it or not, with a small amount of SCSS and no JavaScript, we can build a pure CSS replacement of the Wow.js library, which I hereby christen “Web-Slinger.css.” It feels good to use the scroll-driven optimized standards already supported by some major browsers to make a prototype library. Here’s the finished demo and then we will break down how it works. I have always enjoyed the deliberately lo-fi aesthetic of the original Wow.js page, so it’s nice to have an excuse to create a parody. Much profession, so impress.

Teach scrolling elements to roll over and stay

Web-Slinger.css introduces a set of class names in the format .scroll-trigger-n and .on-scroll-trigger-n. It also defines --scroll-trigger-n custom properties, which are inherited from the document root so we can access them from any CSS class. These conventions are more verbose than Wow.js but also more powerful. The two types of CSS classes decouple the triggers of our one-off animations from the elements they trigger, which means we can animate anything on the page based on the user reaching any scroll marker.

Here’s a basic example that triggers the Animate.css animation “flipInY” when the user has scrolled to the <div> marked as .scroll-trigger-8.

<div class="scroll-trigger-8"></div>
<img 
  class="on-scroll-trigger-8 animate__animated animate__flipInY" 
  src="https://i.imgur.com/wTWuv0U.jpeg"
>

A more advanced use is the sticky “Cownter” (trademark pending) at the top of the demo page, which takes advantage of the ability of one trigger to activate an arbitrary number of animations anywhere in the document. The Cownter increments as new cows appear then displays a reset button once we reach the final scroll trigger at the bottom of the page.

Here is the markup for the Cownter:

<div class="header">
  <h2 class="cownter"></h2>
  <div class="animate__animated  animate__backInDown on-scroll-trigger-12">
    <br>
    <a href="#" class="reset">🔁 Play again</a>
  </div>
</div>

…and the CSS:

.header {
  .cownter::after {
    --cownter: calc(var(--scroll-trigger-2) + var(--scroll-trigger-4) + var(--scroll-trigger-8) + var(--scroll-trigger-11));
    --pluralised-cow: 'cows';

    counter-set: cownter var(--cownter);
    content: "Have " counter(cownter) " " var(--pluralised-cow) ", man";
  }

  @container style(--scroll-trigger-2: 1) and style(--scroll-trigger-4: 0) {
    .cownter::after {
      --pluralised-cow: 'cow';
    }
  }
  
  a {
    text-decoration: none;
    color:blue;
  }
}

:root:has(.reset:active) * {
  animation-name: none;
}

The demo CodePen references Web-Slinger.css from a separate CodePen, which I reference in my final demo the same way I would an external resource.

Sidenote: If you have doubts about the utility of style queries, behold the age-old cow pluralization problem solved in pure CSS.

How does Web Slinger like to sling it?

The secret is based on an iconic thought experiment by the philosopher Friedrich Nietzsche who once asked: If the view() function lets you style an element once it comes into view, what if you take that opportunity to style it so it can never be scrolled out of view? Would that element not stare back into you for eternity?

.scroll-trigger {
  animation-timeline: view();
  animation-name: stick-to-the-top;
  animation-fill-mode: both;
  animation-duration: 1ms;
}

@keyframes stick-to-the-top {
  .1%, to {
    position: fixed;
    top: 0;
  }
}

This idea sounded too good to be true, reminiscent of the urge when you meet a genie to ask for unlimited wishes. But it works! The next puzzle piece is how to use this one-way animation technique to control something we’d want to display to the user. Divs that instantly stick to the ceiling as soon as they enter the viewport might have their place on a page discussing the movie Alien, but most of the time this type of animation won’t be something we want the user to see.

That’s where named view progress timelines come in. The empty scroll trigger element only has the job of sticking to the top of the viewport as soon as it enters. Next, we set the timeline-scope property of the <body> element so that it matches the sticky element’s view-timeline-name. Now we can apply Ryan’s toggle custom property and style query tricks to let each sticky element trigger arbitrary one-off animations anywhere on the page!

View CSS code
/** Each trigger element will cause a toggle named with 
  * the convention `--scroll-trigger-n` to be flipped 
  * from 0 to 1, which will unpause the animation on
  * any element with the class .on-scroll-trigger-n
 **/

:root {
  animation-name: run-scroll-trigger-1, run-scroll-trigger-2 /*etc*/;
  animation-duration: 1ms;
  animation-fill-mode: forwards;
  animation-timeline: --trigger-timeline-1, --trigger-timeline-2 /*etc*/;
  timeline-scope: --trigger-timeline-1, --trigger-timeline-2 /*etc*/;
}

@property --scroll-trigger-1 {
  syntax: "<integer>";
  initial-value: 0;
  inherits: true;
}
@keyframes run-scroll-trigger-1 {
  to {
    --scroll-trigger-1: 1;
  }
}

/** Add this class to arbitrary elements we want 
  * to only animate once `.scroll-trigger-1` has come 
  * into view, default them to paused state otherwise
 **/
.on-scroll-trigger-1 {
  animation-play-state: paused;
}

/** The style query hack will run the animations on
  * the element once the toggle is set to true
 **/
@container style(--scroll-trigger-1: 1) {
  .on-scroll-trigger-1 {
    animation-play-state: running;
  }
}

/** The trigger element which sticks to the top of 
  * the viewport and activates the one-way  animation 
  * that will unpause the animation on the 
  * corresponding element marked with `.on-scroll-trigger-n` 
  **/
.scroll-trigger-1 {
  view-timeline-name: --trigger-timeline-1;
} 

Trigger warning

We generate the genericized Web-Slinger.css in 95 lines of SCSS, which isn’t too bad. The drawback is that the more triggers we need, the larger the compiled CSS file. The numbered CSS classes also aren’t semantic, so it would be great to have native support for linking a scroll-triggered element to its trigger based on IDs, reminiscent of the popovertarget attribute for HTML buttons — except this hypothetical attribute would go on each target element and specify the ID of the trigger, which is the opposite of the way popovertarget works.

<!-- This is speculative — do not use -->
<scroll-trigger id="my-scroll-trigger"></scroll-trigger>
<div class="rollIn" scrolltrigger="my-scroll-trigger">Hello world</div>

Do androids dream of standardized scroll triggers?

As I mentioned at the start, Bramus has teased that scroll-triggered animations are something we’d like to ship in a future version of Chrome, but it still needs a bit of work before we can do that. I’m looking forward to standardized scroll-triggered animations built into the browser. We could do worse than a convention resembling Web-Slinger.css for declaratively defining scroll-triggered animations, but I know I am not objective about Web Slinger as its creator. It’s become a bit of a sacred cow for me so I shall stop milking the topic — for now.

Feel free to reference the prototype Web-Slinger.css library in your experimental CodePens, or fork the library itself if you have better ideas about how scroll-triggered animations could be standardized.

Recent Articles

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here