Viktor Ahmeti.1 year ago
It’s every Dashboard. It’s every WordPress Theme. It’s every single Landing Page. Hail, the Count-Up Animation:
It would sure be great if we could do this directly with CSS but unfortunately it’s not possible at the moment without using a little bit of JavaScript. For those of you with a fried attention span, try it live here or just go see the code on GitHub.
We are creating a library – code that you and other developers will use. This means that we have to write good and clean code. To write good code we must think before we start writing, answering these two questions, in this exact order:
Let’s answer the first question. Developers will use data-attributes to specify the target number in an element, like so:
<span data-targetnumber="410"></span>
They will leave the inside of the element empty because we will populate it with JavaScript, constantly increasing the number until we reach the target number. To identify all the elements that need to be animated, it’s easiest to do it with a CSS class. Let’s call it count-up:
<span class="count-up" data-targetnumber="410"></span>
That’s good enough for the HTML. Now, whenever these elements become visible on the screen they should be animated. But how long should the animation last? What happens when the animation ends? We could just hard code these things but it’s great if we give developers the option to customize them. To initialize our library, the developer should be able to do this:
CountUpAnimation.init(2500);
This will trigger the code in our library, meaning the developer decides when our code starts – not necessarily immediately once the our script loads. Furthermore, the developer is always (always) interested on events. These are things that occur throughout the execution of our code. The only reasonable event a developer would like to know is the finish event which triggers whenever the animation on an element finishes. Thus, the developer should also be able to do this:
CountUpAnimation.onFinish((element) => {
element.classList.add('cool-effect');
});
As you can see, the developer uses the onFinish() method to register a callback that adds a class to the element. Cool. Let us now make this a reality.
Since our library is very simple, we will create a single JavaScript Class called CountUpAnimation inside of which we will set static properties and static methods. From now on, everything will be created inside the curly braces below:
class CountUpAnimation{
//...code here
}
The class needs to save some global information, like how long animations should last, how many times the variable should be incremented, the callback for when the animation finishes, etcetera:
class CountUpAnimation{
//the elements on the page that should be animated
static elements;
//how many times a variable should be incremented
static numberOfIterations = 100;
//how long should the animation last
static animationDuration;
static onFinishCallback = () => {};
}
A variable of interest in the code above is numberOfIterations
. If the user has a target number of 420, how many times should we change the number until we arrive at 420? We could do 0, 210, 420 with 3 iterations or do 1, 5, 200, 310, 420, with 5 iterations. This is obviously too little so we’ll set a constant value of 100 iterations which should be enough for a short animation.
The most basic function is the function updateElement() which puts a specific number inside a specified element:
static updateElement(element, number, displayOptions){
let formattedNumber;
if(displayOptions?.countMode == 'floating'){
formattedNumber = (Math.round(number * 100) / 100).toFixed(displayOptions.decimalPlaces);
}
else{
formattedNumber = Math.floor(number);
}
element.textContent = formattedNumber.toLocaleString();
}
The function accepts the element, the number to write inside it, and also a special object called displayOptions. We will see how we make this object next, but it specifies whether we should display the number as an integer or a floating point number, and also the number of decimal places that should be displayed. This is important to consider since if the user writes 45 it would be weird if we counted like 0, 15.22245, 16.9, 40.0002, 45. This is why we take into consideration the number of decimal places the user may be interested in.
To detect whether an element appears on the screen we will use IntersectionObserver. This is an API provided to us by all modern browsers and it’s an easy way to test for the presence of an element on the screen. We use a common pattern:
class CountUpAnimation{
//...
//the observer
static observer = new IntersectionObserver((entries) => {
this.intersectionHandler.call(this, entries);
}, {threshold: 0.3});
//the callback for when an entry intersects
static intersectionHandler(entries){
entries.forEach((entry) => {
if(entry.isIntersecting){
this.countUp(entry.target);
}
});
}
//...
}
The callback calls the countUp() function which starts the counting (we’ll implement it next) and the observer calls it using the call method in order to set the this keyword during function execution. This is crucial here, and I’d suggest reading more about the this keyword. Let’s now implement the counting function step-by-step.
static countUp(element){
if(isNaN(element.dataset.targetnumber)){
console.error('The target number provided is not a valid number!');
this.updateElement(element, element.dataset.targetnumber);
this.onFinishCallback(element);
return;
}
}
The first step was to check for faulty input, in which case we don’t animate the element at all but simply set the value, and we also don’t forget to immediately call the callback that the animation has finished. Let’s continue now with the displayOptions object that we mentioned above.
static countUp(element){
if(isNaN(element.dataset.targetnumber)){
console.error('The target number provided is not a valid number!');
this.updateElement(element, element.dataset.targetnumber);
this.onFinishCallback(element);
return;
}
//convert the target number to a Number data type
let targetNumber = Number(element.dataset.targetnumber);
//the default display options: integer without decimal places
let displayOptions = {
countMode: 'integer',
decimalPlaces: 0
}
//if it's not an integer, find the number of decimal places
if(!Number.isInteger(targetNumber)){
displayOptions.countMode = 'floating';
displayOptions.decimalPlaces = targetNumber.toString().split(".")[1].length || 0;
}
}
The code above is rather straightforward, of course with the decimal places part found on StackOverflow. Now we’ll start the actual animation using setInterval() and ending it after we reach the final number (we’ll omit the above code and write just the next part).
//the current number to be displayed
let currentNumber;
//how many iterations we did
let iterationCount = 0;
//how often the interval should run
let intervalDuration = this.animationDuration / this.numberOfIterations;
//the actual interval
let interval = setInterval(() => {
//if we reached the end, finish everything up
if(iterationCount == this.numberOfIterations){
clearInterval(interval);
this.updateElement(element, Number(element.dataset.targetnumber), displayOptions);
this.observer.unobserve(element);
this.onFinishCallback(element);
return;
}
//if we still haven't reached the end, get the next number and display it
currentNumber = this.getNextNumber(++iterationCount, targetNumber);
this.updateElement(element, currentNumber, displayOptions);
}, intervalDuration);
The above block of code is pretty dense. Notice how we calculate the duration of the interval, how we clear the interval with clearInterval() once we’re over, and how we unobserve() the element once the animation’s finished. This brings us to the most important function of them all.
β οΈ WARNING: THIS SECTION CONTAINS SOME MATHEMATICS
The function I’m talking about is the getNextNumber() function which returns the next number in the iteration, depending on the number of iterations and the target number. This function is important because it should answer one important question: How should we count?
Suppose we have to count up to the number 10 and we have to do it in 4 iterations, we could do any of these:
And hundreds of other combinations. Which one should we choose? This is where some mathematical concepts like linear, exponential, and logarithmic functions come into play.
We can count with equal increments, with the numbers increasing equally, gradually, boringly. By remembering the good old y = ax + b and substituting some of our values, we can implement the function like so:
static getNextNumber(currentIteration, targetNumber){
return (targetNumber / this.numberOfIterations) * currentIteration;
}
With this implementation the animation will look a bit boring, as in the following video:
This gradual increase doesn’t offer anything much interesting. A logarithmic function will actually look better because it starts of fast and then slows down, like below:
And to implement the function, we can do the following:
static getNextNumber(currentIteration, targetNumber){
return targetNumber * Math.log(currentIteration) / Math.log(this.numberOfIterations);
}
There is a whole class of functions called BΓ©zier curves which give a predictable and easy way to define the flow of functions like this, something you may have also seen in controlling the timing of CSS Animations. For the end, don’t forget to also implement the onFinish() function which just initializes the callback:
static onFinish(onFinishCallback){
this.onFinishCallback = onFinishCallback;
}
With that, everything is finished and we’re ready to use our code. Add some valid HTML to your file, include this script, and then initialize the script with something like this:
window.addEventListener('load', () => {
CountUpAnimation.onFinish((element) => {
element.classList.add('cool')
});
CountUpAnimation.init(3500);
});
That was a deep dive on implementing a simple animation like Count Up. You can try the animation live here or you can check out the code on the GitHub Repository. Don’t forget to subscribe to my newsletter below and if you missed my last post check it out here: How to build a Video Preview Feature with JavaScript.
← All blogs