Animate counter from start to end value

I want to put a counter on my site.
The following code works for very large numbers, but low numbers like 3 or 95.5 do not work. But it works with numbers over 1000.Where do you think the problem is with the JavaScript code written?
Thanks in advance for your guidance.

const counters = document.querySelectorAll('.count');
const speed = 200;
counters.forEach((counter) => {
  const updateCount = () => {
  const target = parseInt(counter.getAttribute('data-target'));
    const count = parseInt(counter.innerText);
    const increment = Math.trunc(target / speed);
    if (count < target) {
       counter.innerText = count + increment;
       setTimeout(updateCount, 1);
    } else {
       counter.innerText = target;
    }
  };
 updateCount();
});
<div>
  <div class="counter">
    <div class="counter--inner">
       <span>+</span>
       <span data-target="3" class="count">0</span>
       <span>Years</span>
    </div>
    <p>example1</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
       <span>+</span>
       <span data-target="50000" class="count">0</span>
    </div>
    <p>example2</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
      <span data-target="95" class="count">0</span>
      <span>%</span>
    </div>
    <p>example3</p>                     
  </div>
  
  <div class="counter">
    <div class="counter--inner">
       <span data-target="10000" class="count">0</span>
      <span>%</span>
    </div>
    <p>example4</p>                     
  </div>
</div>

Solution 1:

MDN Docs: The Math.trunc() function returns the integer part of a number by removing any fractional digits.

Therefore in Math.trunc(target / 200)
target has to be at least 200 for you to finally get a 1. But you always get a 0 and you try to do a 0 + 0.


What not to do

  • Don't use textContent to grab values from DOM Nodes on every loop iteration. Simply, increment a variable integer

  • Never use setTimeout/setInterval() set at 1 milliseconds. You're clogging the main thread, and your application performance might suffer from it. Even worse if this operation happens for multiple elements on a single page.

  • Don't animate different counters at different timing durations. A proper implementation would be to animate all counters during a constant time. Going from 0to10 or from 0to9999999 should be "animated" during a constant time duration — for the user to perceive those visual clues/changes and make sense of it, for multiple counter elements.

Animate integers range during defined time

Here's a bulletproof example on how to properly create a animated counters using JavaScript that:

  • uses requestAnimationFrame (instead of setTimeout or setInterval) to not cog the main thread. Warm welcome to performant intervals in JavaScript!
  • uses any element with [data-counter] given a starting textContent integer, and the end value set inside the actual data attribute
  • counts either up or down — depending on the start/end values
  • will not count if the start value equals the end value

const counter = (EL) => {

  const duration = 4000; // Animate all counters equally for a better UX

  const start = parseInt(EL.textContent, 10); // Get start and end values
  const end = parseInt(EL.dataset.counter, 10); // PS: Use always the radix 10!

  if (start === end) return; // If equal values, stop here.

  const range = end - start; // Get the range
  let curr = start; // Set current at start position

  const loop = (raf) => {
    if (raf > duration) raf = duration; // Stop the loop
    const frac = raf / duration; // Get the time fraction
    const step = frac * range; // Calculate the value step
    curr = start + step; // Increment or Decrement current value
    EL.textContent = parseInt(curr, 10); // Apply to UI as integer
    if (raf < duration) requestAnimationFrame(loop); // Loop
  };

  requestAnimationFrame(loop); // Start the loop!
};

document.querySelectorAll("[data-counter]").forEach(counter);
<br>Count from 0 to 10
<br><span data-counter="10">0</span>
<br>You don't necessarily have to start at 0
<br><span data-counter="2022">1000</span>
<br>You can also count in reverse
<br><span data-counter="0">9999</span>
<br>or even to negative values
<br><span data-counter="-1000">1000</span>
<br>This one will not count
<br><span data-counter="666">666</span>
<br>
<br>And for a better UX - all the counters finished simultaneously  during 4000ms

Solution 2:

You're trying to increment zeroes.

const increment = Math.trunc(target / speed);

When your target is less than 200 (which is your hard-coded speed) it will return a 0.

Math.trunc(3/200) will be 0 because according to the MDN Web Docs:

The Math.trunc() function returns the integer part of a number by removing any fractional digits. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc

That's why it won't work on lower numbers than 200.

What you can do is remove the Math.trunc and increment those decimal values instead.