DEV Community

Omar
Omar

Posted on • Edited on • Originally published at tinloof.com

How to build a stopwatch with HTML, CSS, and plain JavaScript (Part 2)

Stopwatch built with HTML, CSS, and JS


Build your own stopwatch with HTML, CSS, and JavaScript

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:

  1. Basics of the Date object
  2. 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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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; 
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Now we can easily use template literals to output our time:

// output: 2:46:39
console.log(`${hh}:${mm}:${ss}`);
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Now that we create the variable timerInterval, we can create a pause function:

function pause (){
    clearInterval(timerInterval);
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

Our stopwatch works perfectly fine!

Top comments (6)

Collapse
 
habeebullahi01 profile image
Habeebullahi01

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/

Collapse
 
bnsddk profile image
Omar

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. πŸ™πŸΌ

Collapse
 
habeebullahi01 profile image
Habeebullahi01

I'll definitely go through the project and make the necessary changes as soon as I can. Thanks again, I'm very grateful.

Thread Thread
 
bnsddk profile image
Omar

Pleasure!

Collapse
 
bnsddk profile image
Omar

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.

Collapse
 
habeebullahi01 profile image
Habeebullahi01 • Edited

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.