28th June 2020 • 12 min read
How to build a stopwatch with HTML, CSS, and plain JavaScript (Part 2)
Omar Benseddik
This series of articles is made out of two parts:
In the first part, we built the stopwatch's user interface with HTML and CSS.
In this second part, we'll make the user interface functional with JavaScript (the stopwatch works).
Final result
This is what we will build:
Here's the link to the GitHub repo.
Here's the link to the stopwatch itself.
If you want to practice with JavaScript, you can download RunJS, a JS notepad that serves as an alternative to the browser's console.
Before we get started with the stopwatch's functionality, let's take some time to understand two fundamental concepts about time in JavaScript:
- Basics of the Date object
- How to format time
Basics of the Date object
Let's get familiar with some concepts.
In JavaScript, the global object Date is used to manage date and time.
We can use the Date() object to get the moment elapsed since January 1, 1970, 00:00:00 UTC, which is also known as the Unix time or Epoch time.
There is a built-in function in JavaScript called Date.now() that returns the number of milliseconds elapsed since then:
1// output: 1590946806933
2Date.now();
The above means that the number of milliseconds elapsed between Epoch time and the moment I wrote Date.now() is 1590946806933.
I type Date.now() a second time and get the following timestamp:
1// output: 1590946936532
2Date.now();
We get a larger number this time because more time elapsed compared to the first time I wrote Date.now().
If I keep refreshing, the timestamp will just get bigger. Every second it will grow by 1000.
This means that between the first time I wrote Date.now() and the second time I wrote Date.now(), the following milliseconds elapsed:
11590946806933 - 1590946936532 = 129599 milliseconds
This is the foundation of the stopwatch we will build.
The moment we click on the Play button, we will store a timestamp in a variable, and another function will constantly run to calculate the difference between Date.now() and that variable we created.
If you still don't understand exactly how the stopwatch will work, don't worry, there will be plenty of explanation later.
How to format time in JavaScript
Format time from milliseconds to other units (hours, minutes, seconds)
Let's say we already obtained a time difference in milliseconds between two dates:
1// (time in ms)
2let time = 10000000;
We want to convert that number to a formatted string with the format hh:mm:ss (hh stands for hours, mm for minutes, and ss for seconds).
How to do that?
We know that 1 hour is equal to 3600000 ms (1000 x 60 x 60).
So dividing 'time' by 3600000 should give us the number of hours in it:
1// output: 2.7777777777777777
2let diffInHrs = time / 3600000;
diffInHrs is 2 hours and 0.7777777777777777 hours.
This will not fit in our hh:mm:ss format.
To get hh, we get the integer value of diffInHrs using Math.floor (which returns the largest integer smaller than 2.7777, i.e. 2):
1// output: 2.7777777777777777
2let diffInHrs = time / 3600000;
3
4// output: 2
5let hh = Math.floor(diffInHrs);
We know that 1 hour is 60 minutes, so let's convert the remaining decimal part of diffInHrs:
1// output: 46.6666666
2let diffInMin = (diffInHrs - hh) * 60;
diffInMin is 46 min and 0.666666 min.
To get mm and ss we follow the same logic for hh keeping in mind that 1 minute is 60 seconds:
1// output: 2.7777777777777777
2let diffInHrs = time / 3600000;
3
4// output: 2
5let hh = Math.floor(diffInHrs);
6
7// output: 46.6666666
8let diffInMin = (diffInHrs - hh) * 60;
9
10// output: 46
11let mm = Math.floor(diffInMin);
12
13// output: 39.9999999943
14let diffInSec = (diffInMin - mm) * 60;
15
16// output: 39
17let ss = Math.floor(diffInSec);
Now we can easily use template literals to output our time:
1// output: 2:46:39
2console.log(`${hh}:${mm}:${ss}`);
We want to be able to reuse that later, so let's write a function timeToString that converts a given time from ms to a formatted string (hh:mm:ss):
1function timeToString(time) {
2 let diffInHrs = time / 3600000;
3 let hh = Math.floor(diffInHrs);
4
5 let diffInMin = (diffInHrs - hh) * 60;
6 let mm = Math.floor(diffInMin);
7
8 let diffInSec = (diffInMin - mm) * 60;
9 let ss = Math.floor(diffInSec);
10
11 return `${hh}:${mm}:${ss}`;
12}
Format time to double-digits format
The output we get ("2:46:39") is correct, but we can already predict an obstacle.
By looking at the result, we can see that when we get a single digit (e.g. 2 hours), it shows "2" and not "02".
Our stopwatch is designed to take double-digits with the following format "00:00:00".
Let's use padStart() to have double-digits instead of single-digits:
1function timeToString(time) {
2 let diffInHrs = time / 3600000;
3 let hh = Math.floor(diffInHrs);
4
5 let diffInMin = (diffInHrs - hh) * 60;
6 let mm = Math.floor(diffInMin);
7
8 let diffInSec = (diffInMin - mm) * 60;
9 let ss = Math.floor(diffInSec);
10
11 let formattedHH = hh.toString().padStart(2, "0");
12 let formattedMM = mm.toString().padStart(2, "0");
13 let formattedSS = ss.toString().padStart(2, "0");
14
15 return `${formattedHH}:${formattedMM}:${formattedSS}`;
16}
17
18// output: "02:46:39"
You can read more about padStart().
We now know how to get a time unit in JavaScript and format it to different units.
Add a click event listener
Note: a click event listener is a function called whenever the user clicks on the element attached to it.
We want to make the stopwatch functional by making the Play, Pause, and Reset buttons work.
We will use addEventListener to call the relevant function whenever we click on a button:
1document.getElementById("playButton").addEventListener("click", start);
2document.getElementById("pauseButton").addEventListener("click", pause);
3document.getElementById("resetButton").addEventListener("click", reset);
To avoid calling document.getElementById multiple times in the future, we can store the button elements in variables:
1let playButton = document.getElementById("playButton");
2let pauseButton = document.getElementById("pauseButton");
3let resetButton = document.getElementById("resetButton");
4
5playButton.addEventListener("click", start);
6pauseButton.addEventListener("click", pause);
7resetButton.addEventListener("click", reset);
Note that we haven't created the "start", "pause", and "reset" functions yet, clicking on the buttons won't result in anything.
Create setInterval
The stopwatch will capture the time elapsed between Date.now() stored in a variable the moment we click on the button and a new Date.now() that will be updated every second.
When we click on the Play button, we'll create a variable (let's name it startTime) that will store the timestamp.
What we want to display instead of "00:00:00" is the formatted time difference between an automatically refreshing Date.now() and startTime, which stores a timestamp.
Let's imagine startTime is 0 and Date.now() is 0 too, then it will display "00:00:00".
After 1000 milliseconds (1 second), startTime will remain as "0", but the new Date.now() will output "1000", therefore the difference of 1000 milliseconds will output "00:00:01" on the page.
After 2000 milliseconds (2 seconds), startTime will remain as "0", but the new Date.now() will output "2000", therefore the difference of 2000 milliseconds will output "00:00:02" on the page.
And so on.
To constantly update Date.now(), we use a JavaScript method called setInterval.
This method calls a function at specified time intervals.
To calculate the time difference every second, we do the following:
1setInterval(function printTime() {
2 let elapsedTime = Date.now() - startTime;
3}, 1000);
Every 1000 milliseconds, a function will call elapsedTime, which stores Date.now(), that is constantly changing.
We are now ready to create the start, pause and reset functions.
Create the start function
Since we'll display the result in the div containing "00:00:00", let's create an id and call it display:
1<body>
2 <div class="stopwatch">
3 <h1><span class="gold">GOLD</span> STOPWATCH</h1>
4 <div class="circle">
5 <span class="time" id="display">00:00:00</span>
6 </div>
Let's create a function so when we click on the Play button, we start counting the time:
1let startTime;
2let elapsedTime;
3
4function start() {
5 startTime = Date.now();
6 setInterval(function printTime() {
7 elapsedTime = Date.now() - startTime;
8 document.getElementById("display").innerHTML = timeToString(elapsedTime);
9 }, 1000);
10}
We first declare two variables: startTime and elapsedTime.
We then create a function and store Date.now() in the startTime variable as we discussed earlier.
Then, using setInterval, we display elapsedTime, which is the difference between Date.now() refreshed every 1000 milliseconds and the variable startTime.
To change the HTML on the page, we use the innerHTML property.
Excellent! The stopwatch works, but we are not done yet.
The time moves slowly, so instead of displaying hours, minutes, and seconds, we'll display minutes, seconds, and milliseconds:
1function timeToString(time) {
2 let diffInHrs = time / 3600000;
3 let hh = Math.floor(diffInHrs);
4
5 let diffInMin = (diffInHrs - hh) * 60;
6 let mm = Math.floor(diffInMin);
7
8 let diffInSec = (diffInMin - mm) * 60;
9 let ss = Math.floor(diffInSec);
10
11 let diffInMs = (diffInSec - ss) * 1000;
12 let ms = Math.floor(diffInMs);
13
14 let formattedMM = mm.toString().padStart(2, "0");
15 let formattedSS = ss.toString().padStart(2, "0");
16 let formattedMS = ms.toString().padStart(2, "0");
17
18 return `${formattedMM}:${formattedSS}${formattedMS}`;
19}
diffInMs will always be anywhere between 0 and 999. If we want to display them in 2 digits instead of 3, we can divide it by 10 (or just multiply by 100 instead of 1000):
1function timeToString(time) {
2 let diffInHrs = time / 3600000;
3 let hh = Math.floor(diffInHrs);
4
5 let diffInMin = (diffInHrs - hh) * 60;
6 let mm = Math.floor(diffInMin);
7
8 let diffInSec = (diffInMin - mm) * 60;
9 let ss = Math.floor(diffInSec);
10
11 let diffInMs = (diffInSec - ss) * 100;
12 let ms = Math.floor(diffInMs);
13
14 let formattedMM = mm.toString().padStart(2, "0");
15 let formattedSS = ss.toString().padStart(2, "0");
16 let formattedMS = ms.toString().padStart(2, "0");
17
18 return `${formattedMM}:${formattedSS}:${formattedMS}`;
19}
And we change the setInterval delay argument from 1000 milliseconds to 10 milliseconds so it refreshes more often:
1function start(){
2 startTime = Date.now();
3 setInterval(function printTime(){
4 elapsedTime = Date.now() - startTime;
5 document.getElementById("display").innerHTML = timeToString(elapsedTime);
6 }, 10)
7}
The stopwatch works well. When we click on the Play button, the stopwatch starts counting elapsed minutes, seconds, and milliseconds.
Let's separate our printing logic into a separate print function to improve readability:
1function print(txt) {
2 document.getElementById("display").innerHTML = txt;
3}
4
5function start() {
6 startTime = Date.now();
7 setInterval(function printTime() {
8 elapsedTime = Date.now() - startTime;
9 print(timeToString(elapsedTime));
10 }, 10);
11}
Next, we want to make the Pause button appear the moment we click on the Play button. Let's modify our start function:
1function start() {
2 startTime = Date.now();
3 setInterval(function printTime() {
4 elapsedTime = Date.now() - startTime;
5 print(timeToString(elapsedTime));
6 }, 10);
7 playButton.style.display = "none";
8 pauseButton.style.display = "block";
9}
Excellent, it works. When we click on the Play button, it switches to the Pause button.
Let's separate this logic into a showButton function that we can reuse later:
1function showButton(buttonKey) {
2 const buttonToShow = buttonKey === "PLAY" ? playButton : pauseButton;
3 const buttonToHide = buttonKey === "PLAY" ? pauseButton : playButton;
4 buttonToShow.style.display = "block";
5 buttonToHide.style.display = "none";
6}
If we call this function with showButton("PLAY"), it will display the Play button and hide the Pause button.
If we call this function with showButton("PAUSE"), it will display the Pause button and hide the Play button.
Let's call it in our start function:
1function start() {
2 startTime = Date.now();
3 setInterval(function printTime() {
4 elapsedTime = Date.now() - startTime;
5 print(timeToString(elapsedTime));
6 }, 10);
7 showButton("PAUSE");
8}
Create the pause function
Now that our start function works, let's create the pause function.
The reason the stopwatch is constantly active is because of the setInterval in the start function. The Date.now() keeps getting refreshed and its output grows, and so does elapsedTime.
To pause the stopwatch, we need a way to stop setInterval.
The global method clearInterval does just that.
To call clearInterval, we need to modify the start function and store setInterval in a variable:
1let startTime;
2let elapsedTime;
3let timerInterval;
4
5function start(){
6 startTime = Date.now();
7 timerInterval = setInterval(function printTime(){
8 elapsedTime = Date.now() - startTime;
9 print(timeToString(elapsedTime));
10 }, 10)
11}
Now that we create the variable timerInterval, we can create a pause function:
1function pause() {
2 clearInterval(timerInterval);
3}
It works! However, when we click on the Pause button, we want to display the Play button so we can activate the stopwatch again.
Let's modify our pause function:
1function pause (){
2 clearInterval(timerInterval);
3 showButton("PLAY");
4}
When we click on the Pause button, the Pause button disappears and the Play button appears.
However, when click on Play after we clicked on Pause for the first time, the stopwatch starts from zero instead of starting from the recorded time where we left it at.
To understand this, let's illustrate in a simpler version of reality where Date.now() is initially 0:
1// start, startTime = Date.now() = 0
2
3// Date.now() = 0
4// elapsedTime = Date.now() - startTime = 0
5print("00:00:00");
6
7// Date.now() = 1
8// elapsedTime = Date.now() - startTime = 1
9print("00:01:00");
10
11// Date.now() = 2
12// elapsedTime = Date.now() - startTime = 2
13print("00:02:00");
14
15// pause
16// wait 3 seconds
17
18// start, startTime = Date.now() = 5
19
20// Date.now() = 5
21// elapsedTime = Date.now() - startTime = 0
22print("00:00:00");
23
24// Date.now() = 6
25// elapsedTime = Date.now() - startTime = 1
26print("00:01:00");
The problem is that when we run our start function after pause, it sets the startTime to that time (i.e. 5).
This is not accurate because our startTime is supposed to where we left it at, in this case, 2. To make that happen, we can just delete the elapsed time from it and make elapsedTime start from 0:
1let startTime;
2let elapsedTime = 0;
3let timerInterval;
4
5function start() {
6 startTime = Date.now() - elapsedTime;
7 timerInterval = setInterval(function printTime() {
8 elapsedTime = Date.now() - startTime;
9 print(timeToString(elapsedTime));
10 }, 10);
11 showButton("PAUSE");
12}
The stopwatch will no longer start from zero when we click on Play after clicking on Pause.
Create the reset function
Great, both our start and pause functions work, now let's create a reset function.
Similarly to the pause function, reset will clearInterval, however, it will replace any captured time with "00:00:00", and regardless of the displayed button (Play or Pause), it will display the Play button.
It will also set back elapsedTime back to "0".
In the previous part of this series, we gave the Reset button the id resetButton.
Now let's create the reset function:
1function reset() {
2 clearInterval(timerInterval);
3 print("00:00:00");
4 elapsedTime = 0;
5 showButton("PLAY");
6}
Our stopwatch works perfectly fine!