From Net Magazine: On-scroll Animations and Transitions by FINE Developer Eman

Posted in News


In Net magazine's October 2018 print issue FINE senior front-end developer Emmanuel Garcia walks through the process of using the IntersectionObserver API to execute on-scroll animations and transitions.
All the files you need for this tutorial can be found here.

Animate Energetic Scrolling Transitions
By Emmanuel Garcia

The IntersectionObserver API is a promising and exciting new web API that tackles one of the most common issues for web developers: detecting the visibility of elements when they come into view. This API helps simplify the problem by providing a more modern solution for detecting element intersections. IntersectionObserver has many use cases for implementing deferred functionality, including lazy loading images, as well as triggering animations and transitions.

According to specs, “The IntersectionObserver API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.” This modern web API allows you to observe and trigger a callback function when an element is visible in the viewport or intersects with another element. Some of the previous difficulties developers have had with implementing on scroll animations or transitions is the heavy reliance on resource-intensive scroll events to detect when an element appears in the viewport. With the amount of complex calculations needed to solve for this, you would ultimately have to lean on third-party libraries to get the job done.

This is where IntersectionObserver comes in. It allows you to easily register a callback that is executed when an element being observed intersects with another element or with the viewport.

In this tutorial, I will explain how to set up and configure an IntersectionObserver to observe multiple elements at once, as well as watch the callbacks to trigger some nifty animations once elements intersect with the viewport.

** API Setup**

To use the IntersectionObserver API you simply need element(s) to the observe() function. You can also pass a config object into the Observer’s constructor to configure with the following:

  • root: defines the root element used as ‘capture frame’ for intersection checking. This defaults to the viewport but can be any element.
  • rootMargin: defines margin set around root. Supports same values as CSS margins. Extends or minimises intersection point.
  • threshold: a value or array of values between 0 and 1. It corresponds to the ratio of visibility of an element, with 0 meaning completely out of view and 1 being 100 per cent in view. IntersectionObserver is direction agnostic, so it will be triggered when an element enters and leaves ‘capture frame’. In the case of multiple values, the intersection callback is triggered when each threshold value is met.

Here is a quick sample of what that code would look like:

`let config = {
root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport
rootMargin: '0px', // margin around root. Values are similar to css property. Unitless values not allowed
threshold: 0.5 // visible amount of item shown in relation to root. Can also be an array [0, .25, .5, 1]

let observer = new IntersectionObserver((entries) => {

}, config);


To observe multiple elements, you can use the same IntersectionObserver but you’ll have to iterate through all of them and separately observe each element.

const sections = document.querySelectorAll('.section');

sections.forEach(section => {

Tutorial Setup

In this tutorial we’ll use the IntersectionObserver API to observe multiple elements at once and trigger various animations and transitions as each respective panel scrolls into view. We’ll use CodePen for this tutorial, with a fairly straightforward HTML structure. Start by creating the initial container element where we will add our section panels.

<div class=”container”> <div class=”sections”> <!-- section markup goes here --> </div> </div>

Each section contains an img element with photography taken from a specific location, as well as header text containing the image location name.

<div class="section" data-bgcolor="#C1A5A9"> <h2 class="section--header">Kénitra, Morocco</h2> <div class="section--image"><img src="" /></div>

As you can see, this setup is pretty simple. One thing to note is that we use a custom data attribute called data-bgcolor for each panel in order to animate the body background colour as each section scrolls into view. We then pull in the data attribute to our IntersectionObserver callback function in order to execute this functionality.

Sass and CSS Variables

We use SASS (SCSS) as a preprocessor for this tutorial. You can easily set this in CodePen settings. Also, we’ll add some CSS custom property variables to set some default styles, which can easily be reused throughout our SASS code. CSS variables are pretty nifty as they are somewhat similar to SASS variables but you can also pull in these custom property values into JavaScript and update accordingly. I have also imported a Google font for this specific tutorial.

`@import url('');

:root {
--color-text: #fff;
--color-bg: #ddd;
--font-text: 'Nanum Gothic Coding', monospace;
--fontsize-text: 8vw;

Body Styles

Here we use some of our CSS variables to set some global styles for text colour and background colour. We also add a CSS transition to the body element in order to animate and ease in the background colour when it switches on scroll.

`body {
min-height: 100vh;
color: #000;
color: var(--color-text);
background-color: #fff;
background: var(--color-bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background .3s;


Each section div uses vh units to take up 100 per cent of the minimum height of the viewport. This is an easy way to make sure each panel has enough room to scroll through and allow images to display with its natural height.

`.section {
position: relative;
min-height: 100vh;

Section Images

For the images, we align them to the right of each section and set them to opacity: 0. We also set a CSS transition for opacity so as to fade in/out the image as it scrolls into view using IntersectionObserver.

.section--image {
display: block;
position: relative;
max-width: 100%;
margin: 10vh 0 30vh auto;
opacity: 0;
transition: opacity .3s;

.active & {
opacity: 1;

img {
display: block;
position: relative;
max-width: 90%;
margin: 0 0 0 auto;


Section Text

The section location header has a couple of CSS variables in use for font-size and font-family. These can easily be changed by updating the global CSS variables we created earlier. The text is set to position: fixed in order to keep the element in the same position in the viewport as you scroll. We also apply some relative length units for padding and bottom properties. Vmax units translate to a percentage of the width or height of the viewport, whichever is the larger dimension. For the animation, we use a sequence with keyframes to fade in the text and slide up at the same time.

.section--header {
font-size: calc(var(--fontsize-text));
font-family: var(--font-text);
position: fixed;
bottom: 5vmax;
left: 0;
padding-left: 5vmax;
z-index: 1000;
line-height: 1;
font-weight: 300;
opacity: 0;
animation-duration: .65s;
animation-fill-mode: both;

.active & {
animation-name: fadeInUp;

The CSS animation is pretty straightforward as you are setting the opacity to 0 and using the transform property to move the element down to start. Once the animation sequence finishes, it will set opacity to 1 and reset transform to 0. This in turn creates a fluid CSS animation that takes .65s to complete.

`@keyframes fadeInUp{
0% {
transform: translate3d(0,55%,0);
} to {
transform: translateZ(0);

Java Script Setup

For the final touch, we use the IntersectionObserver API outlined previously to detect when each section scrolls into view. We also set the threshold value to 0.5 so that observer will fire when 50 per cent of the element intersects with the viewport.

`const sections = document.querySelectorAll('.section');

let config = {
rootMargin: '0px',
threshold: 0.5

let observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {

}, config);

sections.forEach(section => {

By creating our IntersectionObserver instance we are able to access these entries through the callback function. This array of observed entry types are known as IntersectionObserverEntry.

Through this interface we are provided with three different rectangles that help determine an element’s positioning and boundaries. This information is calculated asynchronously – a big boost for performance as we no longer have to calculate these types of values ourselves. The IntersectionObserverEntry interface also provides us with a handy property called isIntersecting. With this, we can determine when an element enters the ‘capture frame’ and then run our intersectionHandler function.

`function intersectionHandler(entry) {
const current = document.querySelector('');
const next =;
const header = next.querySelector(".section--header");

if (current) {
if (next) {
next.classList.add('active');"--color-bg", next.dataset.bgcolor);


Within our handler function we are setting an active class to each current section that comes into view, which in turn activates our CSS animations and transitions that we declared earlier. By accessing and updating CSS variables in a clean and concise manner, we’re also updating body background colour, which creates a simple but effective transition effect on the front end.


The IntersectionObserver API is a straightforward tool for detecting element intersections in the viewport and has many performance benefits compared to older techniques relying on scroll events. There are also quite a few other use cases that are worth diving into, including lazy loading of images, as well as deferred loading to allow you to run certain functionality only when it’s in view.

IntersectionObserver also has decent support in modern browsers and there is a great polyfill available for other browsers that don’t fully support it yet.


*Article originally published in the October issue of Net magazine. *

More News