Exploring non-destructive scroll-linked CSS animations

If by some miracle you’ve read my past blog posts relating to website animation effects, you’ll know that I have an issue with websites that implement solutions that are reliant on JavaScript yet lack any fallback for when this fails.

If you visit these websites and disable JavaScript you’re often left with a blank page. I don’t wish to assume the motives of other developers but I’ve never felt at ease relying on JavaScript for core functionality without any other fallback.

It’s this mindset that led to my previous attempts with animated effects. While both examples do the job intended, they’re not ideal for more versatile scroll-based animations.

Taking what I’d learnt on these experiments I knew I could build something better…

See the Pen Non-destructive Scroll Animations by Kean Richmond (@keanr) on CodePen.

Clicking to 0.5x view is recommended

What’s going on…

The above is a very simple example showing a few elements fade into view from different X/Y positions when the user scrolls these into the viewport.

The code for this is incredibly lightweight, mostly because it’s not trying to be a one-size-fits-all solution and because the IntersectionObserver API does a lot of the work for us.

Here’s a rundown of the code highlights…

Detecting IntersectionObserver

All our animations rely on the IntersectionObserver API being supported. As we can’t detect this within our CSS file, we append the <body> tag with the js–io class when this is supported.

Positioning this minified version of this code high within our <head> we’re able to set the js–io class quickly and avoid any elements that shouldn’t appear from flashing onto screen which is possible if using an external file for the script.

By using this class within our CSS selectors that declare our animations, we ensure that browsers that don’t support this feature, or have JavaScript disabled do not render these styles.

Also, as all our JavaScript relating to this functionality is located in the <head> we reduce the possibility of the animations not running at all due to external files not loading, loading slowly or failing due to errors in the code.

Required classes

Alongside the js–io class we also use to-animate and intersect classes.

The to-animate class is used within our HTML to flag that an element is to be animated into view using IntersectionObserver. Combined with the js–io class our CSS selectors set the opacity and transform properties of these elements to their starting positions.

The intersect class is then added to each element that has the to-animate class as it is scrolled into the viewport. With this class appended our CSS triggers the transitions that bring the element into view.

Clears observer once run

With many other animation/effects libraries you’ll find the animations repeat each time an element enters the viewport. While this effect is delightful once, it’s often annoying the more it’s repeated.

To address this, once we’ve added our intersect class, triggering the animation, we clear the IntersectionObserver so that it’s no longer observing this particular element. The result is an element that animates once and remains in its standard position until refresh.

Setting intelligent defaults with the opportunity to customise

We expect 90% of any animations using this code will change the opacity and position of the element using transform:translate();

Setting these as default we limit the need to specify transitions for every element and by combining custom properties for the transform property, we’re able to reduce the amount of CSS required even further.

Yet, if a developer wishes to utilise more complex transitions, or even switch to keyframe animation they can easily unset our defaults and switch in what animations they require without the need for additional JavaScript to target these elements specifically.

@media (prefers-reduced-motion:no-preference){

  .js--io .section--1.intersect {transition-timing-function:cubic-bezier(.175,.885,.32,1.275);}
  .js--io .section--2.intersect {opacity:0; transition:none; animation: 1.5s linear forwards fadeMotion;}


@keyframes fadeMotion {
  to {opacity:1; transform:translate(0);}


Given how simple it is to employ the prefers-reduced-motion media query it would be careless for us to not utilise this.

With fallbacks already in place for older browsers and non-JS users we know that by encasing all our animation CSS within this media query that users with this setting enabled are catered for without any additional work beyond the media query.

Not plug & play

Unlike other libraries I’ve seen, this code isn’t 100% plug and play. It offers only limited transitions and requires a developer to add additional CSS to customise beyond this.

But what this is, is an incredibly lightweight and fault tolerant way of adding simple animation effects to your website should you have the desire and ability.

This is by no means complex code, yet providing non-JS fallbacks for these kinds of simple effects seems to be non-existent or just very well hidden. Given how easy it seemed to get to this result makes we wonder if the majority of developers have just given up on progressive enhancement techniques altogether?


While this code provides a good solution based on current browser support, in the future I expect that scroll-linked animation through use of the @scroll-timeline rule will succeed many techniques reliant on JavaScript, delivering a truly CSS only solution.

See https://css-tricks.com/practical-use-cases-for-scroll-linked-animations-in-css-with-scroll-timelines/ for examples of this in action.

We'd love to hear from you!

If you think Bronco has the skills to take your business forward then what are you waiting for?

Get in Touch Today!


Add a Comment