Intersection Observer: Multiple elements with same class, run animation on visible elements only

I have the following code, which is an animated counter.

It contains multiple elements with the same class.

I want the counter animation to run only when each element enters the viewport; and only on the element that just entered the viewport.

In other words, as the user scrolls down the page, the numbers animate when they enter the viewport.

At the moment, as soon as the first element enters the viewport, the animation runs on all elements with the same class; and does not run again until the user returns to the top of the page (and then all elements animate again, all at once).

What is the solution to this?

<html>
<body>

<span class="countup">1000</span>
<div style="height: 400px"></div>
<span class="countup">2000</span>
<div style="height: 400px"></div>
<span class="countup">3000</span>

</body>
<script>
// How long you want the animation to take, in ms
const animationDuration = 2000;
// Calculate how long each ‘frame’ should last if we want to update the animation 60 times per second
const frameDuration = 1000 / 60;
// Use that to calculate how many frames we need to complete the animation
const totalFrames = Math.round( animationDuration / frameDuration );
// An ease-out function that slows the count as it progresses
const easeOutQuad = t => t * ( 2 - t );

// The animation function, which takes an Element
const animateCountUp = el => {
    let frame = 0;
    const countTo = parseInt( el.innerHTML, 10 );
    // Start the animation running 60 times per second
    const counter = setInterval( () => {
        frame++;
        // Calculate our progress as a value between 0 and 1
        // Pass that value to our easing function to get our
        // progress on a curve
        const progress = easeOutQuad( frame / totalFrames );
        // Use the progress value to calculate the current count
        const currentCount = Math.round( countTo * progress );

        // If the current count has changed, update the element
        if ( parseInt( el.innerHTML, 10 ) !== currentCount ) {
            el.innerHTML = currentCount;
        }

        // If we’ve reached our last frame, stop the animation
        if ( frame === totalFrames ) {
            clearInterval( counter );
        }
    }, frameDuration );
};

// Run the animation on all elements with a class of ‘countup’
const runAnimations = () => {
    const countupEls = document.querySelectorAll( '.countup' );
    countupEls.forEach( animateCountUp );
};


// Intersection Observer: 

const onIntersection = (entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        console.log(entry);
        runAnimations();
      }
    }
  };
  
  const observer = new IntersectionObserver(onIntersection);
  observer.observe(document.querySelector('.countup'));
</script>
</html>

Credit to James Shakespeare for the count-up animation.

i think it would be easier if you add the class only when the respective element is in the viewport and toggle off if its out of it(if it is your intention to stop the animation at that point)

Thanks @Sylvant . This is what I have come up with, however I am still having no luck. Do you have any suggestions? I appreciate your help!

// How long you want the animation to take, in ms
const animationDuration = 2000;
// Calculate how long each ‘frame’ should last if we want to update the animation 60 times per second
const frameDuration = 1000 / 60;
// Use that to calculate how many frames we need to complete the animation
const totalFrames = Math.round( animationDuration / frameDuration );
// An ease-out function that slows the count as it progresses
const easeOutQuad = t => t * ( 2 - t );

// The animation function, which takes an Element
const animateCountUp = el => {
	let frame = 0;
	const countTo = parseInt( el.innerHTML, 10 );
	// Start the animation running 60 times per second
	const counter = setInterval( () => {
		frame++;
		// Calculate our progress as a value between 0 and 1
		// Pass that value to our easing function to get our
		// progress on a curve
		const progress = easeOutQuad( frame / totalFrames );
		// Use the progress value to calculate the current count
		const currentCount = Math.round( countTo * progress );

		// If the current count has changed, update the element
		if ( parseInt( el.innerHTML, 10 ) !== currentCount ) {
			el.innerHTML = currentCount;
		}

		// If we’ve reached our last frame, stop the animation
		if ( frame === totalFrames ) {
			clearInterval( counter );
		}
	}, frameDuration );
};

// Run the animation on all elements with a class of 'runcounter'
const runAnimations = () => {
	const countupEls = document.querySelectorAll( '.runcounter' );
	countupEls.forEach( animateCountUp );
};

const onIntersection = (entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        console.log(entry);
		var element = document.getElementById('.countup');
		element.classList.add('.runcounter');
        runAnimations();
		element.classList.remove('.runcounter');
      }
    }
  };
  
  const observer = new IntersectionObserver(onIntersection);
  observer.observe(document.querySelector('.countup'));

im not experienced with applying animation to my pages, so its hard for me to work up the code atm. What i can see is, your runAnimation function selects all elements with the class “runcounter” and runs the animation(for all of them). So whenever you toggle the class for single element, all elements with that class get the animation initiated(at least thats how i was able to interpret it). You should instead have the animation initiated only for the element which acquired the class. My understanding is, or at least i assume you can make the class itself own the animation style and whenever its added to an element, the animation would start for it(but dont cite me on that part, as i said i havent used such properties yet for my projects).
Also your code has some typos, like you name classes with dots- '.runcounter' , when it should be just the class name 'runcounter' . Same for when you select element by Id. Only the query selector i assume utilize the css selector syntax.