Custom animated Counter

Custom animated Counter

A counter could be a nice to have to make your website look nicer.

Let's start with our HMTL

We are going to use data-sets to set the desired number to count to and the duration it should take animating.

<h2 data-count-to="25" data-duration="3500" class="counter">0</h2>

We will dynamically parse this data-set values with Javascript

Javascript part

Let's start by creating 3 helper functions to parse the data-sets and update the counter text (the number).

const getNumber = (counter) => {
  return parseFloat(counter.dataset.countTo);
};

const getSpeed = (counter) => {
  return parseFloat(counter.dataset.duration);
};

const updateTex = (counter, text) => {
  counter.textContent = text;
};

Here is where the magic happens

Now let's build our animation function which will take 3 parameters: counter, countTo and duration. This function should run until our count reaches the desired number and should take our desired duration time. For this we need to create two variables startTime initialised to null and currentTime initialised to the current date (time)

  let startTime = null;

  let currentTime = Date.now();

Next, we will create our step function.

 const step = (currentTime) => {
    if (!startTime) {
      startTime = currentTime;
    }

    const progress = Math.min((currentTime - startTime) / duration, 1);

    const currentNum = Math.floor(progress * countTo);

    updateTex(counter, currentNum);

    if (progress < 1) {
      window.requestAnimationFrame(step);
    } else {
      window.cancelAnimationFrame(window.requestAnimationFrame(step));
    }
  };

requestAnimationFrame will call our function around 60 times per second this way our animation will look smoother.

Worth mentioning that Math.min returns the lowest-valued number passed into it. In our case this will run until it gets to 1.

When progress reaches 1 means we arrived at our desired number (progress * countTo) and we will stop our animation.

All the pieces together


const getNumber = (counter) => {
    return parseFloat(counter.dataset.countTo);
};
const getSpeed = (counter) => {
    return parseFloat(counter.dataset.duration);
};

const updateTex = (counter, text) => {
    counter.textContent = text;
};

const animate = (counter, countTo, duration) => {
    let startTime = null;

    let currentTime = Date.now();

    const step = (currentTime) => {
        if (!startTime) {
            startTime = currentTime;
        }

        const progress = Math.min((currentTime - startTime) / duration, 1);

        const currentNum = Math.floor(progress * countTo);

        updateTex(counter, currentNum);

        if (progress < 1) {
            window.requestAnimationFrame(step);
        } else {
            window.cancelAnimationFrame(window.requestAnimationFrame(step));
        }
    };

    window.requestAnimationFrame(step);
};

const counters = document.querySelectorAll('.counter');
counters.forEach((counter) => {
    const countTo = getNumber(counter);
    const animationDuration = getSpeed(counter);
  animate(counter, countTo, animationDuration);
});

Demo

Repo