CSS Animations laggy or jumpy when using Intersection Observer API

Issue

in my HTML/CSS web project I wanted to include a CSS-based animation using @keyframes, which worked like a charm.
Then I tried adding a possibility to only load the animations when they are visible to the viewer using IntersectionObserver – Intersection Observer API.

For that I followed this tutorial –
(section: "Add the class when the element is scrolled into view")

So far so good but here comes my Problem:

I have three classes that are sliding in from the right and are supposed to stop at different widths.
For some reason though as soon as I add the intersection observer, the animation is super laggy. They slide in, suddenly stop, slide a little bit more and then jump into the final position instead of sliding smoothly as they were before.

Here is what I have done:

const observer = new IntersectionObserver(entries => {
    // Loop over the entries
    entries.forEach(entry => {
      // If the element is visible
      if (entry.isIntersecting) {
        // Add the animation class
        entry.target.classList.add('rectangle-animation-one','rectangle-animation-two','rectangle-animation-three');

      }
    });
  });
  
  observer.observe(document.querySelector('.rectangle-one'));
  observer.observe(document.querySelector('.rectangle-two'));
  observer.observe(document.querySelector('.rectangle-three'));
.section-flex{
  display:flex;
  flex-direction: column;
  padding: 30px;
}
.rectangle-one  {
  margin-left: auto;
  height: 125px;
  line-height: 125px;
  width: 80%;
  font-size: 20px;
  color: #fff;
  text-align: center;
  background: #EC6F72;
  box-shadow: 0px 7px 24px 3px rgba(0, 0, 0, 0.25);
}
.rectangle-animation-one {
  animation-duration: 1s;
  animation-name: slidein-one;
  animation-iteration-count: 1;
  animation-direction:normal;
}
@keyframes slidein-one {
  from {
    margin-left:100%;
    width:300%
  }

  to {
    margin-left:20%;
  }
}
.rectangle-two  {
  margin-top: 0px;
  margin-left: auto;
  height: 125px;
  line-height: 125px;
  width: 55%;
  font-size: 20px;
  color: #fff;
  text-align: center;
  background: #FFB94D;
  box-shadow: 0px 7px 24px 3px rgba(0, 0, 0, 0.25);
}
.rectangle-animation-two {
    animation-duration: 1s;
    animation-name: slidein-two;
    animation-iteration-count: 1;
    animation-direction:normal;
  }
  @keyframes slidein-two {
    from {
      margin-left:100%;
      width:300%
    }
  
    to {
      margin-left:45%;
    }
  }
  
.rectangle-three  {
  margin-top: 0px;
  margin-left: auto;
  height: 125px;
  line-height: 125px;
  width: 40%;
  font-size: 20px;
  color: #fff;
  text-align: center;
  background: #3C51AA;
  box-shadow: 0px 7px 24px 3px rgba(0, 0, 0, 0.25);
}
.rectangle-animation-three {
    animation-duration: 1s;
    animation-name: slidein-three;
    animation-iteration-count: 1;
    animation-direction:normal;
  }
  @keyframes slidein-three {
    from {
      margin-left:100%;
      width:300%
    }
  
    to {
      margin-left:60%;
    }
  }
<div class="sectionflex">
        <div class="rectangle-one">
        <p>TEXT 1</p>
        </div>
        <div class="rectangle-two">
        <p>TEXT 2</p>
        </div>
        <div class="rectangle-three">
        <p>TEXT 3</p>
        </div>
    </div>
    <script src="app.js"></script>

Any ideas on what I’m doing wrong here? I’m almost certain its the way I’m trying to add all three animations in the javascript code, I’ve tried all sorts of combinations but can’t get it right.

Thanks for any help

Solution

Okay, I’ll be honest, there could be a bunch of reason why your code above isn’t doing the thing you think it is, but I think mainly because, be it transformed or placed with margins etc, your off-screen content stradles the line between getting on screen, which ruins your animation. I would suggest using a simpler setup to make this work.

In general, we want to wrap your content inside another element that doesn’t move, but is used a the trigger for appearing on screen. When the wrapping (section in my example) section intersects, it will add a class, which should trigger a transition on your children.

Transitions are useful when something toggles between two states – it means you don’t really have to keep in mind the animation yourself. Animations are best used when you need multiple keyframes with different values, but here, there are two states with a value each.

This would look something like this:

const observer = new IntersectionObserver(entries => {

    // Loop over the entries
    entries.forEach(entry => {
      
      // Let's just toggle a class here
      // We can respond to it however we want in CSS
      entry.target.classList.toggle('in-view', entry.isIntersecting);

    });
    
});
  
observer.observe(document.querySelector('section#one', { treshold: 1 }));
observer.observe(document.querySelector('section#two', { treshold: 1 }));
observer.observe(document.querySelector('section#three', { treshold: 1 }));
main {
  display: flex;
  flex-direction: column;
  padding: 30px;
  align-items: flex-end;
}
/* I have made this 100vh high, just so we can see the effect properly. Remove the 100vh below to see something more akin to your original. */
section {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: flex-end;
}
/* I have made this bit shared, so we don't get lost in the repeated CSS. It's fine otherwise, but this way we don't focus on the wrong thing. */
section > div {
  height: 125px;
  line-height: 125px;
  width: 80%;
  font-size: 20px;
  color: #fff;
  text-align: center;
  background: #EC6F72;
  box-shadow: 0px 7px 24px 3px rgba(0, 0, 0, 0.25);
  transform: translateX(100vw);
  /* Instead of an animation, perhaps a transition will do better. */
  transition: transform .4s;
}
section.in-view > div {
  /* When a section contains in view, the div inside it will slide in using a smooth transform. */
  transform: translateX(0);
}
section#two > div  {
  width: 55%;
  color: #fff;
  background: #FFB94D;
}
section#three > div  {
  width: 40%;
  color: #fff;
  background: #3C51AA;
}
<main>
  <!--
  So we need to split this up, one wrapping <section> (the type of element doesn't matter) will be used to detect when the content should come into view.
  -->
  <section id="one">
    <div>
      <p>TEXT 1</p>
    </div>
  </section>
  <section id="two">
    <div>
      <p>TEXT 2</p>
    </div>
  </section>
  <section id="three">
    <div>
      <p>TEXT 3</p>
    </div>
  </section>
</main>

I hope this helps a bit. I know it’s not directly solving the animation, but animations are hard enough to pull off, so if you can solve it with a transition, that is usually the most reasonable route to take.

Update

After seeing your response and answer, I do think it’s good to point out that, no you do need three ResizeObservers (don’t do that, it will make you code messy very quickly!) You are trying to add specific classes to trigger specific animations, so all you need to do is find a way to use the same code to accomplish this. In this case I added a data-name="one" to all three of your boxes, so we can concatenate a string to generate a class like rectangle-animation-one.

const observer = new IntersectionObserver(entries => {
  // Loop over the entries
  entries.forEach(entry => {
    // If the element is visible
    if (entry.isIntersecting) {
      // Add the animation class
      entry.target.classList.add('rectangle-animation-' + entry.target.dataset.name, entry.isIntersecting);
    }
  });
});

observer.observe(document.querySelector('.rectangle-one'));
observer.observe(document.querySelector('.rectangle-two'));
observer.observe(document.querySelector('.rectangle-three'));
/*
const observer2 = new IntersectionObserver(entries => {
  // Loop over the entries
  entries.forEach(entry => {
    // If the element is visible
    if (entry.isIntersecting) {
      // Add the animation class
      entry.target.classList.add('rectangle-animation-two', entry.isIntersecting);
    }
  });
});

observer2.observe(document.querySelector('.rectangle-two'));

const observer3 = new IntersectionObserver(entries => {
  // Loop over the entries
  entries.forEach(entry => {
    // If the element is visible
    if (entry.isIntersecting) {
      // Add the animation class
      entry.target.classList.add('rectangle-animation-three', entry.isIntersecting);
    }
  });
});
observer3.observe(document.querySelector('.rectangle-three'));
*/
.section-flex{
  display:flex;
  flex-direction: column;
  padding: 30px;
}
.rectangle-one  {
  margin-left: auto;
  height: 125px;
  line-height: 125px;
  width: 80%;
  font-size: 20px;
  color: #fff;
  text-align: center;
  background: #EC6F72;
  box-shadow: 0px 7px 24px 3px rgba(0, 0, 0, 0.25);
}
.rectangle-animation-one {
  animation-duration: 1s;
  animation-name: slidein-one;
  animation-iteration-count: 1;
  animation-direction:normal;
}
@keyframes slidein-one {
  from {
    margin-left:100%;
    width:300%
  }

  to {
    margin-left:20%;
  }
}
.rectangle-two  {
  margin-top: 0px;
  margin-left: auto;
  height: 125px;
  line-height: 125px;
  width: 55%;
  font-size: 20px;
  color: #fff;
  text-align: center;
  background: #FFB94D;
  box-shadow: 0px 7px 24px 3px rgba(0, 0, 0, 0.25);
}
.rectangle-animation-two {
    animation-duration: 1s;
    animation-name: slidein-two;
    animation-iteration-count: 1;
    animation-direction:normal;
  }
  @keyframes slidein-two {
    from {
      margin-left:100%;
      width:300%
    }
  
    to {
      margin-left:45%;
    }
  }
  
.rectangle-three  {
  margin-top: 0px;
  margin-left: auto;
  height: 125px;
  line-height: 125px;
  width: 40%;
  font-size: 20px;
  color: #fff;
  text-align: center;
  background: #3C51AA;
  box-shadow: 0px 7px 24px 3px rgba(0, 0, 0, 0.25);
}
.rectangle-animation-three {
    animation-duration: 1s;
    animation-name: slidein-three;
    animation-iteration-count: 1;
    animation-direction:normal;
  }
  @keyframes slidein-three {
    from {
      margin-left:100%;
      width:300%
    }
  
    to {
      margin-left:60%;
    }
  }
<div class="sectionflex">
        <div class="rectangle-one" data-name="one">
        <p>TEXT 1</p>
        </div>
        <div class="rectangle-two" data-name="two">
        <p>TEXT 2</p>
        </div>
        <div class="rectangle-three" data-name="three">
        <p>TEXT 3</p>
        </div>
    </div>
    <script src="app.js"></script>

Even better would to to just toggle an animation class, and define them like this in your CSS:

.rectangle-one.animation { ... }

Then you have a truly universal triggering method for your animations.

Hope this helps a bit!

Answered By – somethinghere

Answer Checked By – David Marino (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.