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>
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>
Share
Improve this question
edited Jan 17, 2022 at 20:44
Roko C. Buljan
207k41 gold badges327 silver badges339 bronze badges
asked Jan 17, 2022 at 18:42
h.m.p.frontendDeveloperh.m.p.frontendDeveloper
5051 gold badge6 silver badges14 bronze badges
1
- maybe try with Math.ceil – cmgchess Commented Jan 17, 2022 at 18:53
3 Answers
Reset to default 6Animate counter using CSS only
NOTICE: missing Firefox support (2024.)
You can animate a CSS Var integer by defining a @property and by using animation
property and CSS counter
on the ::before/after
pseudo element:
@property --from {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
.counter {
transition: --from 1s;
counter-reset: int var(--from);
animation: counter var(--time, 1000) forwards ease-in-out;
}
.counter::after {
content: counter(int);
}
@keyframes counter {
to {
--from: var(--to, 100);
}
}
<br>Count from 0 to 100
<br><span class="counter" style="--from:0; --to:100; --time:4s;"></span>
<br>You don't necessarily have to start at 0
<br><span class="counter" style="--from:1000; --to:2024; --time:4s;"></span>
<br>You can also count in reverse
<br><span class="counter" style="--from:9999; --to:0; --time:4s;"></span>
<br>or even to negative values
<br><span class="counter" style="--from:1000; --to:-1000; --time:4s;"></span>
<br>This one will not count
<br><span class="counter" style="--from:666; --to:666; --time:4s;"></span>
Animate counter using JavaScript
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 an integer variableNever use
setTimeout/setInterval()
set at a small milliseconds value. Such will clog 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 a different timing duration. 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 the range during a defined time period
Here's a bulletproof example on how to properly create animated counters using JavaScript:
- use the proper, more performant requestAnimationFrame (instead of setTimeout or setInterval) to not cog the main thread.
- use elements with
[data-counter]
attribute. The counter will considertextContent
as the start value, and count to the end value which is stored in the actualdata-counter
attribute - count either up or down — depending on the start/end values
- will not count if the start value equals the end value
const ease = {
linear: t => t,
inOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
// Find out more at: https://gist.github./gre/1650294
};
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 timeStart = Date.now();
const loop = () => {
let elaps = Date.now() - timeStart;
if (elaps > duration) elaps = duration; // Stop the loop
const norm = ease.inOutQuad(elaps / duration); // normalised value + easing
const step = norm * range; // Calculate the value step
curr = start + step; // Increment or Decrement current value
EL.textContent = Math.trunc(curr); // Apply to UI as integer
if (elaps < 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
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/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.
Try this:
function animateValue(id, start, end, duration) {
const INTERVAL_TIME = 10;
if (duration < 100) {
duration = 100;
} else if (duration > 10000) {
duration = 10000;
}
let obj = document.getElementById(id);
let decimals = (end % 1 != 0) ? (end.toString().split('.')[1] || '').length : 0;
if (end === start) {
obj.innerHTML = start.toFixed(decimals);
return;
}
const stepTime = Math.ceil(duration / INTERVAL_TIME);
let doneLoops = 0;
let quantityPerLoop = end / stepTime;
let increment = (end - start) / stepTime;
let current = start;
const INTERVAL = setInterval(function () {
current += quantityPerLoop;
obj.innerHTML = current.toFixed(decimals);
if ((increment > 0 && current >= end) || (increment < 0 && current <= end)) {
clearInterval(INTERVAL);
obj.innerHTML = parseFloat(end).toFixed(decimals);
}
}, INTERVAL_TIME);
}
window.onload = function () {
animateValue('quantity1', 0, document.getElementById('quantity1').innerText.trim(), 100);
animateValue('quantity2', 0, document.getElementById('quantity2').innerText.trim(), 3000);
animateValue('quantity3', 0, document.getElementById('quantity3').innerText.trim(), 500);
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<table>
<thead>
<th>Product ID</th>
<th>Quantity</th>
</thead>
<tbody>
<tr>
<td>1</td>
<td id="quantity1">500.55</td>
</tr>
<tr>
<td>2</td>
<td id="quantity2">22.31</td>
</tr>
<tr>
<td>3</td>
<td id="quantity3">3800</td>
</tr>
</tbody>
</table>
</body>
</html>
You just need to pass the id of the element that will receive the animation, the initial value, the end value and the duration of the animation.
It's great for using in Ajax request returns.