Can you make a countdown timer in pure CSS?

Chen Hui Jing - Jan 26 '20 - - Dev Community

I must first apologise for the somewhat rhetorical question as the title. About 3 minutes after I wrote it, my brain exclaimed: “This is clickbait! Clearly if you wrote an entire blog post, the answer should be yes, right??”

Which led me to my next thought. When people write such titles, do they end with a negative conclusion, where the answer is no? What are the statistics on article titles like this? I have so many questions!

This is also why I don’t have many friends. Oh well.

Warning, blog post grew ridiculously long. TL:DR of things is, yes you can do it in CSS but there’s a much better way. Involves Javascript, more details here if you want to skip through the CSS stuff.

Why even countdown in CSS?

Okay, I did not think about this topic out of the blue. I have a friend (I hope she thinks I’m her friend). She tweeted her problem:

The way my brain works is to wonder if everything can be built with CSS (the correct answer is no, not really, but you can still try because it’s fun). Even though not everything can nor should be built with only CSS, this timer thing seemed narrow enough to be plausible.

I describe this as a brute-force method, because the underlying markup consists of all the digits from 0 to 9. You then have to animate them to mimic a timer. So maybe it is not the most elegant approach. But it can fulfil the requirements from the tweet!

Here's the list of concepts used for this implementation:

  • CSS transforms
  • CSS animations
  • Flexbox
  • Demo-only: CSS custom properties
  • Demo-only: Selectors

Demo-only just means that it's additional functionality sprinkled on to make the demo slightly more fancy. Feel free to cut it out if, for whatever reason, you want to fork the code and use it somewhere.

The general approach

If you Google “pure CSS countdown”, my approach of listing all the digits in the markup then doing some form of obscuring the irrelevant digits seems to be the most common solution. This is the markup for the 2 digits making up the timer:

<div class="timer">
  <div class="digit seconds">
    <span>9</span>
    <span>8</span>
    <span>7</span>
    <span>6</span>
    <span>5</span>
    <span>4</span>
    <span>3</span>
    <span>2</span>
    <span>1</span>
    <span>0</span> 
  </div><div class="digit milliseconds">
    <span>9</span>
    <span>8</span>
    <span>7</span>
    <span>6</span>
    <span>5</span>
    <span>4</span>
    <span>3</span>
    <span>2</span>
    <span>1</span> 
    <span>0</span>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The idea is to animate the digits from 9 to 0 by vertically scrolling the block of digits and only showing the required digits at any point in time.

Mechanism behind the countdown

CSS transforms

The only CSS properties that are “safe” for animation are transform and opacity. If you’re wondering why that is, allow me to point you to my favourite explanation by Paul Lewis and Paul Irish on High Performance Animations.

To animate my digits <div>s upward, I turned to the trusty translateY property. For this use case, my <div> is only moving along the y-axis anyway.

.selector {
  transform: translateY(0);
}
Enter fullscreen mode Exit fullscreen mode

You could do the same with the translate property, but then you’d have to state the value for the x-axis as well because a single value in translate resolves to the x-coordinate.

.selector {
  transform: translate(3em);
}
/* is equivalent to */
.selector {
  transform: translate(3em, 0);
}
Enter fullscreen mode Exit fullscreen mode

Read more about the transform functions in the CSS Transforms Module Level 1 specification. The actual math is in there, and even if that’s not your cup of tea, there are numerous examples in there that can help with understanding how the properties work.

CSS animations

The next step is to animate the transform over time. Cue CSS animations.

The CSS animation properties offer a pretty decent range of functionality to make such an approach feasible. I know them because I researched this when I tried to animate the SingaporeCSS and React Knowledgeable unofficial official mascots last year.

Keyframes are a critical concept when you do animation. Keyframes are what you use to specify values for the properties being animated at specified points during the entire animation. They are specified with the @keyframes at-rule.

@keyframes seconds {
  0% { transform: translateY(0) }
  10% { transform: translateY(-1em) }
  20% { transform: translateY(-2em) }
  30% { transform: translateY(-3em) }
  40% { transform: translateY(-4em) }
  50% { transform: translateY(-5em) }
  60% { transform: translateY(-6em) }
  70% { transform: translateY(-7em) }
  80% { transform: translateY(-8em) }
  90% { 
    transform: translateY(-10em);
    width: 0;
  }
  100% { 
    transform: translateY(-10em);
    width: 0;
  }
}

@keyframes milliseconds {
  0% {transform: translateY(0) }
  10% { transform: translateY(-1em) }
  20% { transform: translateY(-2em) }
  30% { transform: translateY(-3em) }
  40% { transform: translateY(-4em) }
  50% { transform: translateY(-5em) }
  60% { transform: translateY(-6em) }
  70% { transform: translateY(-7em) }
  80% { transform: translateY(-8em) }
  90% { transform: translateY(-9em) }
  100% { transform: translateY(-9em) }
}
Enter fullscreen mode Exit fullscreen mode

I’ll explain the values after covering the animation properties needed for the countdown.

In my demo, I’ve gone with the shorthand of animation so the code looks like this:

.seconds {
  animation: seconds 10s 1 step-end forwards;
}

.milliseconds {
  animation: milliseconds 1s 10 step-end forwards;
}
Enter fullscreen mode Exit fullscreen mode

If you open DevTools on the demo, and go to the Computed tab (for Firefox or Safari, Chrome displays this list under their box model in Styles), you will see the computed values for each of the different CSS properties used on your page.

Animation properties in DevTools

From there you can see that the animation shorthand I used explicitly covers the following properties:

  • animation-name

This is used to identify the animation, and you can use any combination of case-sensitive letters a to z, numerical digits 0 to 9, underscores, and/or dashes.

The first non-dash character must be a letter though, and you cannot use -- nor reserved keywords like none, unset, initial or inherit to start the name.

  • animation-duration

This sets the length of time your animation should take to complete 1 cycle. So for the seconds column of digits, I set it to 10s while for the milliseconds column of digits, I set it to 1s.

  • animation-iteration-count

This sets the number of times the animation should cycle through before stopping. The seconds column only needs to run once, while the milliseconds column needs to run through its animation cycle 10 times.

  • animation-timing-function

This describes how the animation progresses throughout the duration of each cycle. Timing functions can be fairly granular if you are familiar with cubic-bezier() functions but I most often see people use keyword values for general use-cases.

I used the step-end keyword, which resolves to steps(1, jump-end). The steps() function allows us to have stepped animation, where the first argument indicates the number of stops during the transition. Each stop is displayed for an equal amount of time.

jump-end allows me move my <div> upward in steps instead of a smooth scroll, and pause at the end value of translateY. This is a terrible sentence and even more horrible explanation.

Please refer to Jumps: The New Steps() in Web Animation by Dan Wilson for a much better explanation. Visual demos and code in there!

  • animation-fill-mode

This lets you dictate how a CSS animation applies its styles to the target before and after the animation runs. I wanted the position of my <div>s to remain at the last keyframe when the animation ends, so I set this value to forwards.

For the seconds digit, the last 2 frames don’t need to be shown at all because the timer is not zero-padded. When the countdown hits 9, the seconds digit needs to not show up nor take up space. So those keyframes have an additional width: 0 property on them.

Also, because I went with forwards for the animation-fill-mode, to make the 0 stay on screen at the end of the animation, the last frame for milliseconds remains at -9em.

Read more about CSS animations in the CSS Animations Level 1 specification. It broadly explains how animations work in the context of CSS, then covers in detail each of the individual animation properties. Also, examples of working code aplenty.

Flexbox

This is my favourite part. The requirement is that during the last second, when only the digits 9 to 0 remain on display, the whole timer has to be aligned center.

Here’s where it is time to reveal the Javascript solution, which is honestly, much more straightforward. The key here is Window.requestAnimationFrame(). Here’s the MDN entry for it.

You’re welcome.

let end;
const now = Date.now;
const timer = document.getElementById("timer");
const duration = 9900;

function displayCountdown() {
  const count = parseInt((end - now()) / 100);
  timer.textContent =
    count > 0 ? (window.requestAnimationFrame(displayCountdown), count) : 0;
}

function start() {
  end = now() + duration;
  window.requestAnimationFrame(displayCountdown);
}
Enter fullscreen mode Exit fullscreen mode

This implementation is also so much easier to style, because Flexbox.

<div class="timer-container">
  <p class="timer" id="timer">99</p>
</div>
Enter fullscreen mode Exit fullscreen mode
.timer-container {
  display: flex;
  height: 100vh; /* height can be anything */
}

.timer {
  margin: auto;
}
Enter fullscreen mode Exit fullscreen mode

When I started this post, I already said, just because you can do something with pure CSS doesn’t mean you should. This is the prime example. Anyway, here’s the Codepen with the same enhanced-for-demo-purposes functionality sprinkled on.

But let us continue with the pure CSS implementation, even if it is just an academic exercise at this point.

.timer-container {
  display: flex;
  height: 100vh; /* height can be anything */
}

.timer {
  overflow: hidden;
  margin: auto;
  height: 1em;
  width: 2ch;
  text-align: center;
}

.digit {
  display: inline-block;
}

.digit span {
  display: block;
  width: 100%;
  height: 1em;
}
Enter fullscreen mode Exit fullscreen mode

If you compare this with the Javascript implementation, you’ll notice a lot of similarities.

Yes, my friends. If you had suspected that I was using the modern-day CSS answer to vertical centring on the web, you are absolutely right. Auto-margins is the mechanism in play here.

To be fair, the display: flex and auto-margin on flex child technique centralises the whole timer block. Within the timer itself, the text should be centre-aligned with the text-align property.

Read more about Flexbox in the CSS Flexible Box Layout Module Level 1 specification. It is the definitive resource for how Flexbox works and even though it is fairly lengthy, there are plenty of code examples in there to help you visualise how things work.

Fun demo extra #1: Dynamic colour changing

Another requirement was for the font colour and background colour to be customisable. I’m pretty sure she meant in the code and not on the fly, but since we can do this on the fly, why not?

Cue CSS custom properties and the HTML colour input. Before you ask me about support for the colour input, I shall invoke first strike and display the caniuse chart for it.



Data on support for the input-color feature across the major browsers from caniuse.com

Come on, this is pretty green here. So anyway, declare your custom properties for font colour and background colour like so:

:root {
  --fontColour: #000000;
  --bgColour: #ffffff;
}
Enter fullscreen mode Exit fullscreen mode

Use them in the requisite elements like so:

.timer {
  /* other styles not shown for brevity */
  background-color: var(--bgColour, white);
}

.digit {
  /* other styles not shown for brevity */
  color: var(--fontColour, black);
}
Enter fullscreen mode Exit fullscreen mode

That’s the set up for the timer itself. Now, control these colours with the colour input. Toss in 2 colour inputs into the markup and position them where you like. I went with the top-right corner.

<aside>
  <label>
    <span>Font colour:</span>
    <input id="fontColour" type="color" value="#000000" />
  </label>
  <label>
    <span>Background colour:</span>
    <input id="bgColour" type="color" value="#ffffff" />
  </label>
</aside>
Enter fullscreen mode Exit fullscreen mode

Then, you can hook up the colour picker with the custom properties you declared in the stylesheet like so:

let root = document.documentElement;
const fontColourInput = document.getElementById('fontColour');
const bgColorInput = document.getElementById('bgColour');

fontColourInput.addEventListener('input', updateFontColour, false);
bgColorInput.addEventListener('input', updateBgColour, false);

function updateFontColour(event) {
  root.style.setProperty('--fontColour', event.target.value);
}

function updateBgColour(event) {
  root.style.setProperty('--bgColour', event.target.value);
}
Enter fullscreen mode Exit fullscreen mode

It’s not that much code, and kind of fun to play with in a demo, IMHO.

Fun demo extra #2: Checkbox hack toggle

I could have left the demo to start automatically when the page loaded and letting people refresh the page to start the animation again, but I was going all in with the pure CSS thing, so…

Anyway, checkbox hack plus overly-complicated selectors. That’s how this was done. If you had just gone with Javascript, which is probably the right thing to do, you could used a button with an event listener. But you’re too deep in this rabbit hole now.

I built this bit such that when unchecked, the label shows Start but when the input is checked, the label shows Restart. Because why not make things more complicated?

.toggle span {
  font-size: 1.2em;
  padding: 0.5em;
  background-color: palegreen;
  cursor: pointer;
  border-radius: 4px;
}

input[type="checkbox"] {
  opacity: 0;
  position: absolute;
}

 input[type="checkbox"]:checked ~ aside .toggle span:first-of-type {
  display: none;
}

.toggle span:nth-of-type(2) {
  display: none;
}

input[type="checkbox"]:checked ~ aside .toggle span:nth-of-type(2) {
  display: inline;
}
Enter fullscreen mode Exit fullscreen mode

The actual bit that triggers the animation looks like this:

input[type="checkbox"]:checked ~ .timer .seconds {
  animation: seconds 10s 1 step-end forwards;
}

input[type="checkbox"]:checked ~ .timer .milliseconds {
  animation: milliseconds 1s 10 step-end forwards;
}
Enter fullscreen mode Exit fullscreen mode

With the checkbox hack, the order of the elements on the page does matter because you can only target sibling selectors after an element and not before it. So the checkbox needs to be as near the top (and not nested) as possible.

Wrapping up

Truth be told, I think I’m a terrible technical writer because most of my posts are so long I reckon only a tiny handful of people ever read through the whole thing.

But this is my blog, and not some official documentation, so I’m kinda going to keep doing whatever and writing these ramble-y posts.

At least I try to organise the content into coherent sections? Okay, to be fair, if I was writing for a proper publication, I’d put on my big girl pants and write concisely (like a professional, LOL).

Unfortunately, this is not a proper publication. ¯\_(ツ)_/¯ Anyway, much love if you really made it through the whole thing. Hope at least some of it was useful to you.

Credits: OG:image from autistic.shibe’s instagram

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .