1

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

3
  • Slide with transforms, not margins or positions. Here is the issue you probably experience: observer gets triggered, triggers animation, animation places object outside of viewport, triggers intersection observer etc... If you can, use transforms instead of other values (like left, top and margin) as they don't animate smoothly and require whole redraws and relayouts (because they literally change layouts, which transforms don't). Commented Apr 18, 2022 at 13:10
  • Thanks for the super fast reply @somethinghere, I'm not that familiar with transforms yet, so I will have to look into that. What I don't understand though is if I decide to only animate only one of the blocks and load it when it comes into view, it works perfectly and does not lag at all. Once I add the other two it gets weird. I'm thinking it the way I'm adding the other two blocks in the .js as I just kind of did it the way I thought it would work, not knowing a lot of javascript :D Commented Apr 18, 2022 at 13:30
  • I think @somethinghere gave a complete answer here, and more importantly, I think your assertion of "it works perfectly and does not lag at all" is false. What's probably true is it "does not lag at all" as far as you can PERCEIVE. If your animation method is expensive for redraws, you may not perceive the extra work the browser is doing until that work exceeds the rate at which it can redraw. So you're conceptualizing the way computers work in just an incorrect way, which prevents you from understanding the problem. It isn't a binary of "working, not working". Commented Sep 19, 2024 at 14:05

2 Answers 2

1

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!

Sign up to request clarification or add additional context in comments.

5 Comments

@somethinhere Thx for posting this solution, I played around with it and it also does the trick ;) But that the code I posted was not working kept bugging me because it didn't make any sense... So I kept playing around and figured it was related to my .js code. I simply can't load all the animations from one "const observer" - I had to add const observer2 and 3 for each animation. Again, huge thanks for your solution, knowing there is a way around it, gave me the peace to wrap my head around the error in my code :D
I honestly don't think you need multiple observers though, that's duplicative work that shouldn't be necessary. Curious, but glad it helped. I remember all this stuff being daunting and hours of debugging to realise that it's down to one small detail somewhere. Keep at it :D
Wow! That's so cool - I didn't know there were possibilities like "data-name", really nice to learn about something new here. Now it looks a lot cleaner. Again thanks so much for taking time, I'm so much looking forward to spending uncountable hours of debugging in the future :-P
I'm sure you will and it will be fine. I wish there was still those simple aha moments, they were a damn joy! So much fun when it finally clicks... They're a bit rarer these days, but the ones I do come across still blow my mind.
Haha yes I 100% know what you mean it keeps me motivated during this part of my learning journey. Guess it's a positive way of "chasing the dragon" :D Just checked your website btw, real cool stuff !
1

After playing around with it and getting some help from @somethinghere (Thanks a lot!) I figured out what was going wrong on my side..

For anyone else stuck with the same problem, I couldn't simply add all animations via one constant observer in my .js file. I now added an individual observer const per class and it now looks like this:

EDIT: Please check out the update in the answer by @somethinghere for a cleaner version

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', entry.isIntersecting);
    }
  });
});

observer.observe(document.querySelector('.rectangle-one'));

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">
        <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>

1 Comment

Well done, however... You do not need an resizeobserver for each! I will amend my answer below. I think the issue you are facing is that you named each animation something different (one, two, three), but in that case there are better solutions than repeating your code thrice!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.