Building an Animated Counter with React and CSS

cooljasonmelton

Jason Melton

Posted on October 19, 2020

Building an Animated Counter with React and CSS

In this blog, I attempt to create my own version of an animated counter component that I keep finding all over the web.

I’ll show you how I went about it, but I would love feedback. Do you know a better way to do this?

Leave a comment or shoot me an email at jason.melton2@gmail.com.

demo

the code on Github

Tutorial

Table of Contents

  • Preliminary Junk
  • Count Component
  • Increment Function
  • Conclusion

Preliminary Junk

I set up a create-react-app, deleted a bunch of default stuff, and a file structure like this:

file structure

I added some basic CSS to the App component — height, width, and flex box to center all its contents.

.App {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
Enter fullscreen mode Exit fullscreen mode

I also set up a JSON file containing the data that I will map into Count components.

{
    "counts": [
        {
            "id": "001",
            "label": "A Really Big Number",
            "number": "900000",
            "duration": "2"
        },
        {
            "id": "002",
            "label": "Ice Cream Flavors",
            "number": "5321",
            "duration": "2"
        },
        {
            "id": "003",
            "label": "Customers Served",
            "number": "100",
            "duration": "2"
        },
        {
            "id": "004",
            "label": "Complaints",
            "number": "0",
            "duration": "2"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Count Component

The object of my Count component is to accept some data about how the count should run and render the animation.

First, I set up a basic component.

import React from 'react';

//styling
import './Count.css';

const Count = props => {
  // label of counter
  // number to increment to
  // duration of count in seconds
  const {label, number, duration } = props.data

  return (
    <div className="Count">
      <h3>
        <i>{label}: {number}</i>
      </h3>
    </div>
  );
}

export default Count;

Enter fullscreen mode Exit fullscreen mode

Count gets props of a data item from data.json. I destructured the label, number, and duration from the props.

Using JSX, I return the label and number as a header.

Later, I will change number so that it animates, but for now I can style the hard-coded version of what I’m building.

.Count {
    padding: 2rem;
    margin: 1rem;

    border-radius: 2em;
    box-shadow: 1px 2px 2px #0D3B66;
    background-color: #FAF0CA;

    display: flex;
    align-items: center;
    justify-content: center;

    color: #0D3B66;
}
Enter fullscreen mode Exit fullscreen mode

Increment Function

I set up a function that increments from 0 to the desired number in these three steps:

1) Set up a useState hook that saves our display number and, when updated, will trigger a render of the component.

The hook looks like this:

  // number displayed by component
  const [count, setCount] = useState("0");
Enter fullscreen mode Exit fullscreen mode

I update the JSX to display count instead of number.

  return (
    <div className="Count">
      <h3>
        <i>{label}: {count}</i>
      </h3>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

2) Set up a useEffect hook that calculates the count and increment time.

useEffect() takes an anonymous function that will handle the count. I create variables start and end. start is set to 0.

Initially, I used number as my end. However, for large numbers, this would take all night. Instead, I only increment the first three digits of the number and paste the the rest of back before updating the count.

I calculate the rate of each increment by dividing the duration (seconds) by the number of increments I plan on doing and multiply by 1000 to convert to milliseconds.
Image for post

  useEffect(() => {
    let start = 0;
    // first three numbers from props
    const end = parseInt(number.substring(0,3))
    // if zero, return
    if (start === end) return;

    // find duration per increment
    let totalMilSecDur = parseInt(duration);
    let incrementTime = (totalMilSecDur / end) * 1000;

    // dependency array
  }, [number, duration]);
Enter fullscreen mode Exit fullscreen mode

I was hoping to speed up the interval to make up for how long it would take to increment large numbers, but setInterval() has a minimum duration of 10 milliseconds. Any number less than 10 will reset back to 10.

3) In that same useEffect hook, I employ setInterval() to increment the count with side effect of re-rendering the component.

I add one to start and call setCount() to update my useState hook. I convert start to a string and, if it’s a large number, I concat the rest of the number that I previously chopped off.

    // timer increments start counter 
    // then updates count
    // ends if start reaches end
    let timer = setInterval(() => {
      start += 1;
      setCount(String(start) + number.substring(3))
      if (start === end) clearInterval(timer)       
    }, incrementTime);
Enter fullscreen mode Exit fullscreen mode

The entire component will now look like this:

import React, { useEffect, useState } from 'react';

//styling
import './Count.css';

const Count = props => {
  // label of counter
  // number to increment to
  // duration of count in seconds
  const {label, number, duration } = props.data

  // number displayed by component
  const [count, setCount] = useState("0")

  useEffect(() => {
    let start = 0;
    // first three numbers from props
    const end = parseInt(number.substring(0,3))
    // if zero, return
    if (start === end) return;

    // find duration per increment
    let totalMilSecDur = parseInt(duration);
    let incrementTime = (totalMilSecDur / end) * 1000;

    // timer increments start counter 
    // then updates count
    // ends if start reaches end
    let timer = setInterval(() => {
      start += 1;
      setCount(String(start) + number.substring(3))
      if (start === end) clearInterval(timer)       
    }, incrementTime);

    // dependency array
  }, [number, duration]);

  return (
    <div className="Count">
      <h3>
        <i>{label}: {count}</i>
      </h3>
    </div>
  );
}

export default Count;
Enter fullscreen mode Exit fullscreen mode

Conclusion

I read through several articles about this sort of animation and combined their ideas with my instinct to make this abstract reusable component.

I am not sure what I came up with is the best method. For example setInterval had limitations I didn’t foresee. I would love some feedback. Feel free to comment or shoot me an email at jason.melton2@gmail.com.

Best, Jason

💖 💪 🙅 🚩
cooljasonmelton
Jason Melton

Posted on October 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related