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 at1
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
0
to10
or from0
to9999999
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 startingtextContent
integer, and the end value set inside the actualdata
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.