Creating a Stopwatch in React.js and CSS

Once upon a time I was interviewing for a Principal Front-end Engineer role and was challenged to create an analog clock in vanilla JS. In real time. I was given a little less than an hour to complete the exercise, but crushed it in under 15 minutes. I had never coded a clock before, and was shocked how easy it was. Recently, I found myself wanting to continue that exercise, but with something more complex, and maybe even interactive.

I decided a stopwatch would be the perfect micro-project. The math was basic, but there were some concepts that were new and maybe even tricky.

TLDR: I made a stopwatch in React and CSS and it's pretty sweet. Check it out:

If you're still here, let's walk through the project.

Functional Requirements

The first thing you need to think about when starting on any application is: What is this thing even going to do? What are its functions? After refreshing my knowledge of stopwatch functionality by looking at several online (because what normal person is just using stopwatches regularly?), I decided that my basic requirements should be:

  1. Start a timer by tracking the number of milliseconds elapsed between the start time and "now."
  2. Mark any number of laps, which simply involves saving a list of arbitrary timestamps.
  3. Stop (pause) the timer.
  4. Resume the stopped timer.
  5. Reset the timer to 0.

With those requirements in mind, this is what our API looks like:

const Stopwatch = () => {
  const start = () => {} // also restarts
  const mark = () => {}
  const stop = () => {}
  const reset = () => {}
}

User Interface

The next thing you need to think about is UI functionality. What will the user see? How will she interact with the application? From our functional requirements, I knew I'd need:

  1. Elapsed time readout.
  2. List of marked laps.
  3. Controls to start, stop, resume, and reset the timer, and to mark laps.

From these visual requirements we can infer the basic components we'll need.

A watch face

For pure visual coolness, I want to show the elapsed time as an analog stopwatch with minute, second, and millisecond hands spinning around the center.

  1. Watch hands, one each to indicate minutes, seconds, and milliseconds. These were abstracted into a general Hand component. Each hand will have some common styling, but will be differentiated by color. At any given time, each hand will be rotated according to its value, which we'll achieve by an inline style that applies a transform rule with translateX(-50%), and rotate set to the applicable value via a value prop.

  2. Tick marks: A ring of light gray tick marks are spaced for each second (1/60), darker and thicker tick marks at 5-second intervals, and darker and even thicker tick marks at 15-second intervals. I used zero HTML/JSX elements to make these. They are created using a conical gradient in CSS applied to the ::before pseudo element of the watch face. This was admittedly a little tricky to figure out at first, but the math was ultimately embarassingly simple:

    • 1-second intervals: 360 degrees in a circle divided by 60 seconds in a minute = a tick mark every 1.67 degrees
    • 5-second intervals: 360/12 = every 30 degrees
    • 15-second intervals: 360/4 = every 90 degrees

Thus, my 3 repeating conical gradients in CSS:

background-image: repeating-conic-gradient(
  from 359deg,
  #555 0 2deg, transparent 2deg 90deg      
), repeating-conic-gradient(
  from 359.5deg,
  #555 0 1deg, transparent 1deg 30deg      
), repeating-conic-gradient(
  from 359.75deg,
  #ccc 0 .5deg, transparent .5deg 6deg      
);

This creates something like this:
Conical gradients

Then I'd need a mask to turn these gradients into tick marks by obscuring (or masking) all but the ends of them:

mask-image: radial-gradient(
  circle at center,
  transparent 66%,
  #fff 66.1%
);

Which results in:
Completed tick marks

Controls

I'd need a button bar to show our controls

  1. Start button to start the timer. This button serves double duty as the "Lap" button while the timer is running.
  2. Stop button that pauses the timer.
  3. Reset button that completely resets the Stopwatch component to its original "zeroed" state.

Digital Readout

In addition to the analog clock to also show elapsed time, I decided to add a digital readout (in MM:SS:ss format), because it's more readable. Oddly, this is the meatiest part of our code: converting our elapsed time in milliseconds to the whole minutes, whole seconds, and remaining milliseconds.

I would need to get only the whole minutes and seconds, no remainders and nothing less than 0. I ensure the former by applying Math.floor(value) to always round down to the nearest whole number, and the latter by applying Math.max(0, value) to replace any value less than zero with zero. I saved this as a convenience function, and define some useful constants:

const getNumOrZero = num => Math.floor(Math.max(0, num))
const ONE_SECOND_MS = 1000
const ONE_MINUTE_MS = ONE_SECOND_MS * 60

Whole minutes

Now to get the whole minutes value, I could simply divide the total elapsed milliseconds by the number of milliseconds in a minute (ONE_MINUTE_MS), rounding down to get the whole minutes without the remainder (Math.floor via getNumOrZero()):

const wholeMinutesValue = getNumOrZero(elapsed / ONE_MINUTE_MS)

I'll need this value back in milliseconds later, so I can simply multiply it by ONE_MINUTE_MS:

const wholeMinutesInMs = wholeMinutesValue * ONE_MINUTE_MS

Whole seconds

I then do the same thing to get the whole seconds. I divide the total elapsed milliseconds, minus the wholeMinutesInMs calculated above, by ONE_SECOND_MS (milliseconds in a second). This gives me the number of whole seconds remaining after subtracting the whole minutes:

const wholeSecondsValue = getNumOrZero((elapsed - wholeMinutesInMs) / ONE_SECOND_MS)
const wholeSecondsInMs = wholeSecondsValue * ONE_SECOND_MS

Remaining milliseconds

I can easily get the remaining milliseconds after subtracting the wholeMinutesInMs and wholeSecondsInMs from the total elapsed time in milliseconds:

const millisecsValue = elapsed - wholeMinutesInMs - wholeSecondsInMs

Assembling the digital elapsed time readout

Now I could easily assemble my digital readout, being sure to left pad the minutes and seconds values with a zero for values < 10:

const elapsedFormatted = `${wholeMinutesValue.toString().padStart(2, '0')}:` +
  `${wholeSecondsValue.toString().padStart(2, '0')}:` +
  `${millisecsValue.toString().padStart(3, '0')}`

And I can render this:

Marked Laps

The last UI component is a list of marked laps. I used an ordered list, but in reverse order so that the most recent lap is at the top of the list.

<ol className="time lap" reversed>{ lapList }</ol>

lapList is an array of lap timestamps in the same MM:SS:ss format as the digital readout. Note the reversed HTML attribute, which (as you might suspect) reverses the order of an ordered list.

The finished project

What I ended up with is a simple, slick, functional stopwatch: React/SCSS Stopwatch

And just for fun, I added a dark mode by abstracting the colors into SCSS variables and toggling a class:

I'm pretty pleased with how it turned out. The code is totally straightforward, but if you have any questions just drop them in a comment below!

20