Timer in HTML/CSS/JS without using timer functions
Tags: css, javascript, html
Published: 2019-12-02 00:15:06

Last night, I issued a challenge on Twitter:

{% twitter 1201044286428499969 %}

Today, I'm going to explain [how I solved it.](https://codepen.io/legowerewolf/pen/wvBwQBL)

## Introduction

In the browser, there's different components that handle timing. JavaScript, of course, with the `setInterval()` and `setTimeout()` functions. The rendering engine also handles things that change over time: CSS Animations, which form the core of my solution.

Animations
* can be started and stopped with JavaScript
* have defined, *editable* durations
* trigger an event when they finish

As a bonus, you can make your timer's progress look pretty. I kept it pretty simplistic in my solution: just a solid blue bar.

## HTML

First, let's get some markup going. We're going to need:
* an input element, to get the desired duration
* a button, so we can start the timer
* a an element to animate

```html
<input 
       type="text" 
       id="time" 
       placeholder="0h0m0s" 
       pattern="(\d*h)*(\d*m)*(\d*s)*" required
       >
<button id="start">Start</button>

<div id="bar"></div>
```

Note the `placeholder` and `pattern` properties on the input element. We're expecting a specific input format, and that's how we enforce it.

Now, we need to style it. 

## Styles

The important part is the `#bar`. First, let's make it visible.

```scss
#bar {
  background: blue;
  height: 1em;
  width: 100%;
  margin-top: 2em;
}
```

Now, let's make it animate. First we need to define the animation - here, we're just playing with the width.

```scss

@keyframes timing {
  from { width: 100%; }
  to { width: 0; }
}

#bar {
  background: blue;
  height: 1em;
  width: 100%;
  margin-top: 2em;
}
```

And now we'll tell the bar to animate.

```
@keyframes timing {
  from { width: 100%; }
  to { width: 0; }
}

#bar {
  background: blue;
  height: 1em;
  width: 100%;
  margin-top: 2em;
  
  animation: {
    name: timing;
    timing-function: linear;
    duration: 5s;
    play-state: paused;
  }
}
```

Boom. That's all the CSS we need. (Well, this is SCSS, but you get the idea.)

Now let's script it up.

## JavaScript

```js
// let's get short names for the elements
let input = document.getElementById("time");
let startButton = document.getElementById("start");
let bar = document.getElementById("bar");

// Let's start the timer when someone clicks the start button.
startButton.addEventListener('click', () => {

  // If the input's not valid, stop right here.
  if (!input.validity.valid) return;
  
  // Let's get the value and break it up into hours, minutes, and seconds
  let times = input.value.match(/(\d*h)*(\d*m)*(\d*s)*/);
  
  // And use math to get a seconds value for everything
  let time = [times[3], times[2], times[1]]
    .reduce((accum, curr, index) => 
      accum + (curr ? curr : "").match(/\d*/)[0] * Math.pow(60, index), 0
    );
  
  // Set the animation duration and start it.
  bar.style.animationDuration = `${time}s`;
  bar.style.animationPlayState = "running";
});

// We need to listen for the animation ending
bar.addEventListener('animationend', () => {
  alert('Timer ended');
  
  // Reset the animation
  bar.style.animationPlayState = "paused"; // We don't want to restart immediately
  bar.style.setProperty("animation-name", "_"); // Specify a junk animation name
  void bar.offsetWidth; // Force a reflow
  bar.style.removeProperty("animation-name"); // Clear the animation name
});

```

There's a few bits in here that need further explanation.

How does this work? 
```js
let time = [times[3], times[2], times[1]]
  .reduce((accum, curr, index) => 
    accum + (curr ? curr : "").match(/\d*/)[0] * Math.pow(60, index), 0
  );
```

First off, `times[1]` is the hours from the input. `times[2]` is the minutes and `times[3]` is the seconds.

We feed in the provided time values in reverse order, so seconds is index 0, minutes is index 1, and hours is index 2. Then, using 0 as a starting value, we add the appropriate number of seconds for each component. A simpler way to write this would be:

```js
let time = 0;
time += times[3].match(/\d*/)[0] * Math.pow(60, 0) // notice the power increases? 
time += times[2].match(/\d*/)[0] * Math.pow(60, 1) // it's the index.
time += times[1].match(/\d*/)[0] * Math.pow(60, 2)
```

or even just

```js
let time = 0;
time += times[3].match(/\d*/)[0]          // 1 second per second
time += times[2].match(/\d*/)[0] * 60     // 60 seconds per minute
time += times[1].match(/\d*/)[0] * 3600   // 3600 seconds per hour
```

The other funky code is this part, from the `animationEnd` handler.

```js
bar.style.setProperty("animation-name", "_"); // Specify a junk animation name
void bar.offsetWidth; // Force a reflow
bar.style.removeProperty("animation-name"); // Clear the animation name
```

Let's do it line-by-line. 

```js
bar.style.setProperty("animation-name", "_"); // Specify a junk animation name
```

What's happening here, for anyone unfamiliar with CSS, is we're increasing the specificity of the `animation-name` property. To the renderer, it's like we're covering up the value provided in the stylesheet with this junk value. The next time the page is re-rendered, it'll use this value.

```js
void bar.offsetWidth; // Force a reflow
```

This is the weird one. There's a [great explanation on Reddit](https://www.reddit.com/r/learnjavascript/comments/782qdx/what_does_void_elementoffsetwidth_do/doql343) about this but in essence, it tricks the browser into re-rendering the page by pretending to ask for a property value on an element that has a pending change already from the previous line. The browser *has* to re-render, re-calculate, before it can answer. Without this line, though, the browser will ignore the lines immediately preceding and following this one.

```js
bar.style.removeProperty("animation-name"); // Clear the animation name
```

Remember the first line of this arcane bit of code, how it's covering up the value from the stylesheet? This removes that cover, re-exposing the value to the renderer.

## Conclusion

I think that's about it. If there's any questions, drop a comment here or on the original Twitter thread and I'll do my best to answer.