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.
Time in JavaScript
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:
// output: 1590946806933
Date.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:
// output: 1590946936532
Date.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:
1590946806933 - 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:
// (time in ms)
let 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 * 60 * 60).
So dividing 'time' by 3600000 should give us the number of hours in it:
// output: 2.7777777777777777
let 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):
// output: 2.7777777777777777
let diffInHrs = time / 3600000;
// output: 2
let hh = Math.floor(diffInHrs);
We know that 1 hour is 60 minutes, so let's convert the remaining decimal part of diffInHrs
:
// output: 46.6666666
let 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:
// output: 2.7777777777777777
let diffInHrs = time / 3600000;
// output: 2
let hh = Math.floor(diffInHrs);
// output: 46.6666666
let diffInMin = (diffInHrs - hh) * 60;
// output: 46
let mm = Math.floor(diffInMin);
// output: 39.9999999943
let diffInSec = (diffInMin - mm) * 60;
// output: 39
let ss = Math.floor(diffInSec);
Now we can easily use template literals to output our time:
// output: 2:46:39
console.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):
function timeToString(time) {
let diffInHrs = time / 3600000;
let hh = Math.floor(diffInHrs);
let diffInMin = (diffInHrs - hh) * 60;
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
return `${hh}:${mm}:${ss}`;
}
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:
function timeToString(time) {
let diffInHrs = time / 3600000;
let hh = Math.floor(diffInHrs);
let diffInMin = (diffInHrs - hh) * 60;
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
let formattedHH = hh.toString().padStart(2, "0");
let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
return `${formattedHH}:${formattedMM}:${formattedSS}`;
}
// 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.
Make the stopwatch functional
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:
document.getElementById("playButton").addEventListener("click", start);
document.getElementById("pauseButton").addEventListener("click", pause);
document.getElementById("resetButton").addEventListener("click", reset);
To avoid calling document.getElementById
multiple times in the future, we can store the button elements in variables:
let playButton = document.getElementById("playButton");
let pauseButton = document.getElementById("pauseButton");
let resetButton = document.getElementById("resetButton");
playButton.addEventListener("click", start);
pauseButton.addEventListener("click", pause);
resetButton.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:
setInterval(function printTime() {
let elapsedTime = Date.now() - startTime;
}, 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:
<body>
<div class="stopwatch">
<h1><span class="gold">GOLD</span> STOPWATCH</h1>
<div class="circle">
<span class="time" id="display">00:00:00</span>
</div>
Let's create a function so when we click on the Play button, we start counting the time:
let startTime;
let elapsedTime;
function start(){
startTime = Date.now();
setInterval(function printTime(){
elapsedTime = Date.now() - startTime;
document.getElementById("display").innerHTML = timeToString(elapsedTime);
}, 1000)
}
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:
function timeToString(time) {
let diffInHrs = time / 3600000;
let hh = Math.floor(diffInHrs);
let diffInMin = (diffInHrs - hh) * 60;
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
let diffInMs = (diffInSec - ss) * 1000;
let ms = Math.floor(diffInMs);
let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
let formattedMS = ms.toString().padStart(2, "0");
return `${formattedMM}:${formattedSS}${formattedMS}`;
}
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):
function timeToString(time) {
let diffInHrs = time / 3600000;
let hh = Math.floor(diffInHrs);
let diffInMin = (diffInHrs - hh) * 60;
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
let diffInMs = (diffInSec - ss) * 100;
let ms = Math.floor(diffInMs);
let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
let formattedMS = ms.toString().padStart(2, "0");
return `${formattedMM}:${formattedSS}:${formattedMS}`;
}
And we change the setInterval
delay argument from 1000 milliseconds to 10 milliseconds so it refreshes more often:
function start(){
startTime = Date.now();
setInterval(function printTime(){
elapsedTime = Date.now() - startTime;
document.getElementById("display").innerHTML = timeToString(elapsedTime);
}, 10)
}
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:
function print(txt) {
document.getElementById("display").innerHTML = txt;
}
function start() {
startTime = Date.now();
setInterval(function printTime() {
elapsedTime = Date.now() - startTime;
print(timeToString(elapsedTime));
}, 10);
}
Next, we want to make the Pause button appear the moment we click on the Play button. Let's modify our start function:
function start() {
startTime = Date.now();
setInterval(function printTime() {
elapsedTime = Date.now() - startTime;
print(timeToString(elapsedTime));
}, 10);
playButton.style.display = "none";
pauseButton.style.display = "block";
}
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:
function showButton(buttonKey) {
const buttonToShow = buttonKey === "PLAY" ? playButton : pauseButton;
const buttonToHide = buttonKey === "PLAY" ? pauseButton : playButton;
buttonToShow.style.display = "block";
buttonToHide.style.display = "none";
}
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:
function start() {
startTime = Date.now();
setInterval(function printTime() {
elapsedTime = Date.now() - startTime;
print(timeToString(elapsedTime));
}, 10);
showButton("PAUSE");
}
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:
let startTime;
let elapsedTime;
let timerInterval;
function start(){
startTime = Date.now();
timerInterval = setInterval(function printTime(){
elapsedTime = Date.now() - startTime;
print(timeToString(elapsedTime));
}, 10)
}
Now that we create the variable timerInterval, we can create a pause function:
function pause (){
clearInterval(timerInterval);
}
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:
function pause (){
clearInterval(timerInterval);
showButton("PLAY");
}
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:
// start, startTime = Date.now() = 0
// Date.now() = 0
// elapsedTime = Date.now() - startTime = 0
print("00:00:00")
// Date.now() = 1
// elapsedTime = Date.now() - startTime = 1
print("00:01:00")
// Date.now() = 2
// elapsedTime = Date.now() - startTime = 2
print("00:02:00")
// pause
// wait 3 seconds
// start, startTime = Date.now() = 5
// Date.now() = 5
// elapsedTime = Date.now() - startTime = 0
print("00:00:00")
// Date.now() = 6
// elapsedTime = Date.now() - startTime = 1
print("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:
let startTime;
let elapsedTime = 0;
let timerInterval;
function start() {
startTime = Date.now() - elapsedTime;
timerInterval = setInterval(function printTime() {
elapsedTime = Date.now() - startTime;
print(timeToString(elapsedTime));
}, 10);
showButton("PAUSE");
}
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:
function reset() {
clearInterval(timerInterval);
print("00:00:00");
elapsedTime = 0;
showButton("PLAY");
}
Our stopwatch works perfectly fine!
Top comments (6)
Thanks for the tutorial Omar. I had a great time coding the stopwatch. It was really fun. I did it from scratch and made a few changes and modifications to the code. I added a reset button and a lap recording functionality.
I'll add the link to the github repo and a live site. Please look through it if you can. I'll be glad to learn where I used a bad practice, resources that can help, or anything of that sort.
Repo:
github.com/Habeebullahi01/Stopwatch
Live site:
stopwatch.vercel.app/
Hi Habeebullahi! Glad you enjoyed the article and had fun creating another version.
Overall it looks good, but I would suggest changing the icon colors from black to white (or another color that contrasts better with the background and thus more visible and accessible).
Also, for the lap button icon, perhaps it would be better if it matches with the other icons, so there's consistency.
I had a quick look at the code, I think a code formatter like Prettier (you can download it directly from VSCode if you're using it), will help you format your code to be easier to read. E.g. alignment of the attached image.
And in the README of the GitHub Repo, I would suggest adding an image/GIF, and also a link to the live site you shared with me.
That aside it looks great. 💪🏽
Feel free to follow twitter.com/tinloof if you want to keep up to do date with some tips and tutorials. 🙏🏼
I'll definitely go through the project and make the necessary changes as soon as I can. Thanks again, I'm very grateful.
Pleasure!
Hi Habeebullahi,
I just made a slight change in the JavaScript.
I removed formatTwoDigits function and replaced it by padStart(), I suggest you have a look.
I'll definitely check it out.🤗 Thanks for the update. I have also made the changes you suggested for my version, except the code formatting. I haven't been able to get around to doing it yet. I'm using Atom and when I searched for it, it didn't return any results.