CSS-Animated Countdown Timer with React and TypeScript

A while ago, on an old project, we needed to implement a countdown timer. The specified design looked relatively simple: an outline of a circle with the number of seconds remaining inside. As the time counted down, the text would need to update, and the stroke around the circle would need to disappear at an appropriate rate so it would be completely gone when the time got to zero seconds.

I originally implemented this timer in an Ember project. For the past year, I’ve been working with React, so I decided to try my hand at re-implementing it as a React component.

Defining the Props and State

The first thing I usually do when creating a new React component is to figure out what I want the props and state to look like. This component needs to be able to take in some initial time in seconds (the props) and keep track of the current time as it’s counting down (the state). That’s not a whole lot, so these definitions are pretty short:


type Props = {
  startTimeInSeconds: number;
}

type State = {
  timeRemainingInSeconds: number;
}

The React Component

Now on to the fun part: building the React component! First, I’ll show the entire component. Then we’ll go over a few of the parts on their own.


export class CountdownTimer extends React.Component {
  private timer: any;

  constructor(props: Props) {
    super(props);
    this.state = {
      timeRemainingInSeconds: props.startTimeInSeconds
    };
  }

  decrementTimeRemaining = () => {
    if (this.state.timeRemainingInSeconds > 0) {
      this.setState({
        timeRemainingInSeconds: this.state.timeRemainingInSeconds - 1
      });
    } else {
      clearInterval(this.timer!);
    }
  };

  componentDidMount() {
    this.timer = setInterval(() => {
      this.decrementTimeRemaining();
    }, 1000);
  }

  render() {
    return (
      <div className="countdown-timer">
        <div className="countdown-timer__circle">
          <svg>
            <circle
              r="24"
              cx="26"
              cy="26"
              style={{
                animation: `countdown-animation ${this.props
                  .startTimeInSeconds}s linear`
              }}
            />
          </svg>
        </div>
        <div className="countdown-timer__text">
          {this.state.timeRemainingInSeconds}s
        </div>
      </div>
    );
  }
}

The first part to note is a timer property on the class. This will be assigned when we create the interval timer, and then used to cancel that timer after it counts down to zero.

Next, we have the constructor. It takes the startTimeInSeconds props and uses it to assign the initial value of timeRemainingInSeconds in the state.

Then, there’s our function which handles decrementing the number of remaining seconds. It continues as long as the current time remaining is greater than zero. After that, it will clear our timer interval so that it doesn’t continue to run.

After that, we’re using the componentDidMount lifecycle hook of the React component. At this point, we set up an interval to run our decrementTimeRemaining function every second.

Finally, in the render function, we’re rendering an SVG circle and the text of the number of seconds remaining. On the circle element, we add an explicit style for an animation that will count down for the duration of the timer. We’ve also added a trailing “s” after the seconds number that will be displayed.

Unfortunately, if you look at it now, it won’t look very good at all! The seconds displayed will count down, but the circle won’t do much of anything. Let’s fix that!

Make it Look Real Nice

Now, I think this is the really fun part. Why not try and make the component a little prettier? And, with the way this is written, it hardly does anything without a little help from the CSS animation.


.countdown-timer {
  position: absolute;
  width: 52px;
  height: 52px;
}

.countdown-timer__circle circle {
  stroke-dasharray: 151px;
  stroke-dashoffset: 151px;
  stroke-linecap: butt;
  stroke-width: 4px;
  stroke: #fd4f57;
  fill: none;
}

.countdown-timer__circle svg {
  width: 52px;
  height: 52px;
  transform: rotateZ(-90deg);
}

.countdown-timer__text {
  position: absolute;
  top: 17px;
  width: 52px;

  font-size: 14px;
  font-weight: 600;
  text-align: center;
}

@keyframes countdown-animation {
  from {
    stroke-dashoffset: 0px;
  }
  to {
    stroke-dashoffset: 151px;
  }
}

The class names loosely follow the BEM naming convention. The absolute positioning is used to help easily rotate the circle while keeping it inside the intended bounds. The design specifications had the top of the circle as the “starting point” for the outline, which would disappear in a counter-clockwise direction.

We use the same values in stroke-dasharray and stroke-offset, such that when the animation is done, the timer will appear “empty.” In other words, there will be no visible stroke around the circle.

The countdown-animation defined is the animation that was referenced in the explicit style on the circle element of the React component. It starts at stroke-dashoffset 0, meaning the stroke around the circle will be full, and ends at the 151px value used in the other stroke properties, which is when there will be no visible stroke around the circle.

As a note, this component has been styled to cooperate with numbers no longer than two digits. If you’d like to support timers that last more than 99 seconds, you could increase the size of the circle to allow more text to show in the center. If you do that, the stroke-dasharray and stroke-offset values will need to be increased as the circumference of the circle increases. If you aren’t interested in changing the size of the circle, you might consider moving the text outside of the circle altogether.

Let me know if you have any questions about this component, or if you are able to use it for your work in some way! Happy coding!