Scroll animation in Javascript using IntersectionObserver

Using animation in your portfolio site would be a great way to capture the users attention and make them stay longer on your site.

In this post I will show you how to add a special type of Javascript scroll animation to your site that will be activated when you scroll down/up. This will be coded using a feature called intersection observer. Here is a quick preview of what the end animation looks like:
Alt Text

There is also a Youtube video on this :

Step 1] First lets layout our HTML markup and Styling for a basic card UI

<!DOCTYPE html>
<html lang="en">
 <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel='stylesheet' type='text/css' href='./style.css'>
 </head>
 <body>
    <div class="wrapper">
        <div class="card">
            <div class="image"></div>
            <h2>Profile picture</h2>
            <p>Some text goes here. Some text goes here.Some text goes here.Some text goes here....</p>
        </div>
    </div>
  </body>
<html>

and the style.css looks like:

.wrapper {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: space-around;
    background-color: #5dafb8;
    color:white;
    font-family: Verdana, Geneva, Tahoma, sans-serif;
}

.card {
    height: 50vh;
    border-radius: 13px;
    box-shadow: 20px 40px 33px rgba(0,0,0,0.3);
    padding: 2rem;
    width: 35vh;
    background-color:  #6cc2ce;

}

/* Style a div with gradient as Background image */
.image {
    width: 35vh;
    height: 20vh;
    background-image: linear-gradient(70deg, RoyalBlue   , DarkTurquoise );
    background-size: cover;
    background-position: center center;
    box-shadow:  10px 15px 15px 6px #3891b4;
    border-radius: 15px;

}

In this example I have set the background-image to a gradient:

background-image: linear-gradient(70deg, RoyalBlue , DarkTurquoise );

, you can set a real image here for your project. so this is what it looks like:
Alt Text

Step 2]Now lets add some basic animation in CSS with keyframe animation

First we are going to add basic css animation with keyframe animations. Lets start by targeting the heading (h2 tag) and content text (p tag):

.card h2 {
        /* Animate Heading, fades in from left to right */
    animation: animTitle 2s infinite;
} 

.card p {
        /* Animate Paragraph, fades in from right to left */
    animation: animContent 2s infinite;
}


@keyframes animTitle {
    from {
            /* Add starting values for animation */
            transform: translateX(-50px);
            opacity: 0;
    } 
    to  {
            /* Add Ending values for animation */
            transform: translateX(0);
            opacity: 1;
   } 
 }

 @keyframes animContent {
    from {
            /* Add starting values for animation */
            transform: translateX(50px);
            opacity: 0;
    } 
    to  {
            /* Add Ending values for animation */
            transform: translateX(0);
            opacity: 1;
   } 
 }

As you can see from above, we have two keyframe animation functions namely

@keyframes animTitle {...} and @keyframes animContent { ...}

these are called in the css selector rules

.card h2 {animation: animContent 2s infinite;} and .card p{animation: animContent 2s infinite;}

As you can see they both run for 2 seconds and loop infinitely. These are simple transition/translate on the horizontal x values of the elements.

we are also going to add a special elastic stretch animation for the image. The CSS rule and the animation keyframe function for this will be :

.card .image {
      /* Animate image */
    animation: animImage 2s infinite;
}

@keyframes animImage {
    0% {
      -webkit-transform: scale3d(1, 1, 1);
              transform: scale3d(1, 1, 1);
    }
    30% {
      -webkit-transform: scale3d(1.25, 0.75, 1);
              transform: scale3d(1.25, 0.75, 1);
    }
    40% {
      -webkit-transform: scale3d(0.75, 1.25, 1);
              transform: scale3d(0.75, 1.25, 1);
    }
    50% {
      -webkit-transform: scale3d(1.15, 0.85, 1);
              transform: scale3d(1.15, 0.85, 1);
    }
    65% {
      -webkit-transform: scale3d(0.95, 1.05, 1);
              transform: scale3d(0.95, 1.05, 1);
    }
    75% {
      -webkit-transform: scale3d(1.05, 0.95, 1);
              transform: scale3d(1.05, 0.95, 1);
    }
    100% {
      -webkit-transform: scale3d(1, 1, 1);
              transform: scale3d(1, 1, 1);
    }
  }

I created this animation using an online animation generator called Animista. You can also go there and experiment with other animations as well. The site will generate keyframe code which you can attach to a container you want to animate. Like I have done above (I also renamed the keyframe function).

What we have so far

So far we have all animation controlled by CSS for us to animate on scroll we will have to dabble with Javascript. We also need to reorganise/change some css rules and HTML elements. Thats what we will do next.

Step 3] Make changes to HTML markup before we add intersectionObserver

One of the key problems we have in activating animation via Javascript is accessing the animation keyframe function name and the rule that needs to be applied. In our demo we have the following css rules that achieve this:

.card h2 {
        /* Amimate Heading, fades in from left to right */
    animation: animTitle 2s infinite;
} 

.card p {
        /* Animate Paragraph, fades in from right to left */
    animation: animContent 2s infinite;
}

.card .image {
      /* Animate image */
    animation: animImage 2s infinite;
}

To apply this dynamically in javascript, we will need to abandon these css rules and use htmls data attribute to store the animation values shown above. We are also going to attach a class called "animate" to the three elements that we are going to animte. So the HTML markup for the card will look like:

<div class="wrapper">
        <div class="card">
            <div class="image animate" data-animate="animImage 2s"></div>
            <h2 class="animate" data-animate="animTitle 2s ">Profile picture</h2>
            <p class="animate" data-animate="animContent 2s ">Some text goes here.Some text goes here....</p>

        </div>
  </div>

So the import thing here is the data attribute, for example the image containers data attribute is:

data-animate="animImage 2s"

, here we are creating a data item called animate (this is the postfix after the dash) and setting the value to its animation setting that we previously defined in the css style sheet. If this is a bit strange, you can read more about using data attributes here

We also need to add more content so that we can scroll to activate our animation, so I will duplicate three more card wrappers:

<div class="wrapper">
        <div class="card">
            <div class="image animate" data-animate="animImage 2s"></div>
            <h2 class="animate" data-animate="animTitle 2s ">Profile picture</h2>
            <p class="animate" data-animate="animContent 2s ">Some text goes here.Some text goes here....</p>

        </div>
  </div>
 <div class="wrapper">
        <div class="card">
            <div class="image animate" data-animate="animImage 2s "></div>
            <h2 class="animate" data-animate="animTitle 2s ">Profile picture</h2>
            <p class="animate" data-animate="animContent 2s ">Some text goes here....Some text goes here....Some text goes here....Some text goes here....</p>

        </div>
  </div>
  <div class="wrapper">
        <div class="card">
            <div class="image animate" data-animate="animImage 2s "></div>
            <h2 class="animate" data-animate="animTitle 2s ">Profile picture</h2>
            <p class="animate" data-animate="animContent 2s ">Some text goes here....Some text goes here....Some text goes here....Some text goes here....</p>

        </div>
  </div>
  <div class="wrapper">
        <div class="card">
            <div class="image animate" data-animate="animImage 2s "></div>
            <h2 class="animate" data-animate="animTitle 2s ">Profile picture</h2>
            <p class="animate" data-animate="animContent 2s ">Some text goes here....Some text goes here....Some text goes here....Some text goes here....</p>

        </div>
    </div>

Step 4] Add Javascript's intersection observer functionality to detect scroll position

Intersection observer basically observes elements that you tell it to. It will observe changes in the intersection of a target element with an ancestor element. Our ancestor element is going to be the browser viewport and the target elements that we observe for intersection are the three elements of the card namely .image,p tag and h2 tag.
You can read more about Intersection Observer API here. Please note, in the API documentation they have examples where you define a root element, for our case I have left it out beacause I want it to default to browser viewport (If you dont define a root element, it will assume the ancestor as being the root). So our basic code structure for our use case will look like:

<script>
        const callback = (entries) => {
                   //4] Callback code goes here
         }

        //1]Create a new intersectionObserver object, 
        //which will accept a callback function as 
        //a parameter.

        let observer = new IntersectionObserver(callback);


        //2]Select all elements that have ".animate" 
        //class.In our case we have three 
        //elements (.image,<p> and h<2>).

        const animationItems = document.querySelectorAll('.animate');


          //3]Loop through selected elements and add to the
          //observer watch list.      

          animationItems.forEach(item => {
            observer.observe(item)         
        })


    </script>

Will will add this code inline in the markup, at the very end of the body tag. So to simplify, you have 4 steps

1] Create a IntersectionObserver object
2] Query and select items that you want observed
3] Add the selected items to the watch list of the IntersectionObserver object
4]Provide a callback function that will do something when ever an intersection event occurs. In our case we want it to attach a keyframe animation.

In the above code I did not write the code for the callback function. So thats our next task

Step 5] IntersectionObserver callback function

const callback = (entries) => {

           // The entries variable will contain the list of
           // elements that you are observing. When ever 
           // intersection occurs, you need to do forEach loop 
           // to find which one intersected. 
           // For this we check a flag on the element called "isIntersecting"
            entries.forEach(
                (entry) => {
                    if (entry.isIntersecting) {
                        console.log("The element is intersecting >");
                        //If intersecting then attach keyframe animation.
                       //We do this by assigning the data attribute 
                       //we coded in the markup to the style.animation 
                       //of the element
                        entry.target.style.animation = 
                        entry.target.dataset.animate;
                    } else {
                        //We take of the animation if not in view
                        entry.target.style.animation="none";
                    }
                }
            );       
        }

The callback will be called whenever a inetrsection occurs. The callback will have access to all the elements that we in the observer list. In the call back we have to loop through to find which one intersected. We do this by checking a flag on the element called isIntersecting. If you inspect the IF statement, you will see this.

if (entry.isIntersecting) {...}

If it is intersecting then we attach the keyframe animation to the element, thats what the following line does :

entry.target.style.animation = entry.target.dataset.animate

Here we set the elements style.animation to the fetched data attribute called "data-animate" which was setup in step 3. For example, for the image we would get the string part off the data attribute on the elements markup:

`<div class="image animate" data-animate="animImage 2s"></div>

in this case its "animImage 2s".

The ELSE part of the code removes the animation because it is not intersecting :

entry.target.style.animation="none";

So if you scroll back and forth the animation will run again.

The end product

Hope you enjoyed this tutorial and you can see the final code on codepen

16