Project 09 · Intermediate
Countdown Timer
A countdown to New Year's Day that you can point at any date and time. Every second it works out how long is left and splits it into days, hours, minutes, and seconds. It's mostly maths with milliseconds, plus a timer.
- 3 HTML ideas
- 2 CSS ideas
- 7 JavaScript ideas
Why build it
Working with dates in milliseconds lets you build countdowns for launches, sales, and weddings, "posted 3 hours ago" labels, days-until and age calculators, and deadline reminders. Dates trip up most developers, from months that start at 0 to time zones, and the habit you learn here keeps them manageable.
Skills you'll earn
- Create, read, and compare dates with the Date object
- Turn a number of milliseconds into days, hours, minutes, and seconds
- Always show two digits, like 09, with padStart
- Start a timer and stop it safely, so two never run at once
01
Try it
Play with it first, then read the code that makes it work.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Countdown Timer</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="countdown">
<h1 class="countdown-title">Countdown</h1>
<!-- datetime-local gives a date and time picker, no library needed. -->
<label class="target-label" for="target">Counting down to</label>
<input class="target-input" type="datetime-local" id="target" />
<!-- role="timer" is quiet by default, so it isn't read out every second. -->
<div class="units" role="timer" aria-label="Time left">
<div class="unit">
<span class="unit-value" id="days">0</span>
<span class="unit-label">days</span>
</div>
<div class="unit">
<span class="unit-value" id="hours">00</span>
<span class="unit-label">hours</span>
</div>
<div class="unit">
<span class="unit-value" id="minutes">00</span>
<span class="unit-label">minutes</span>
</div>
<div class="unit">
<span class="unit-value" id="seconds">00</span>
<span class="unit-label">seconds</span>
</div>
</div>
<!-- Only the important moments are announced, like reaching zero. -->
<p class="status" id="status" aria-live="polite"></p>
</main>
<script src="script.js"></script>
</body>
</html>02
How it works
The ideas behind it, one at a time, each next to the code that does it.
HTMLthe structure
- 01
Pick a date and time with
datetime-localAn
<input>'stypedecides what kind of value it takes.type="datetime-local"gives a built-in date and time picker that works with a mouse, a keyboard, and on phones, with no extra code or library. The<label>'sfor="target"matches the input'sid, which names the input for screen readers and lets a click on the label focus it.<!-- datetime-local gives a date and time picker, no library needed. --> <label class="target-label" for="target">Counting down to</label> <input class="target-input" type="datetime-local" id="target" /> - 02
Keep it quiet with
role="timer"A
roletells screen readers what kind of thing an element is.role="timer"says this area counts time and changes on its own. Timers are quiet by default: screen readers don't announce each change, which here would mean reading a new number every second. People can still go to it and hear the time left, named byaria-label="Time left".<!-- role="timer" is quiet by default, so it isn't read out every second. --> <div class="units" role="timer" aria-label="Time left"> - 03
Announce only what matters
Because the timer itself is quiet, there needs to be another way to hear the moments that matter. The status paragraph is an
aria-liveregion, so screen readers read out any text put into it. It stays empty most of the time, and JavaScript only fills it for something worth hearing: "Time's up!" or "Pick a time in the future."<!-- Only the important moments are announced, like reaching zero. --> <p class="status" id="status" aria-live="polite"></p>
CSSthe look
- 04
Wrap boxes with
auto-fitandminmax()grid-template-columnssets the grid's columns.minmax(5.5rem, 1fr)means each column is at least 5.5rem wide and grows to an equal share of any spare space.repeat(auto-fit, ...)makes as many of those columns as fit in the row. So the four boxes sit in one row on a wide screen and wrap onto two rows on a narrow one, without a media query./* Four equal boxes that wrap into two rows on narrow screens. */ .units { display: grid; grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr)); gap: 0.75rem; margin-top: 2rem; } - 05
Stop the numbers jiggling
In most fonts, digits have different widths: a 1 is narrower than an 8. When the seconds change from 11 to 18, the text shifts sideways, and with four boxes updating every second the whole timer would jiggle.
font-variant-numeric: tabular-numsgives every digit the same width, so the numbers stay put./* Every digit the same width, so the numbers don't jiggle as they change. */ .unit-value { display: block; font-size: 2.25rem; font-weight: 700; font-variant-numeric: tabular-nums; }
JavaScriptthe behaviour
- 06
Name each unit in milliseconds
JavaScript measures time in milliseconds: one second is 1000. The code builds each unit from the one before, so
MINUTEis60 * SECOND,HOURis60 * MINUTE, andDAYis24 * HOUR. Naming them once makes the maths below read like words, likemsLeft / DAY, instead of numbers like 86400000. They'reconstbecause they never change.// Dates are counted in milliseconds, so name each unit once. const SECOND = 1000; const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; - 07
Always show two digits with
padStartpadStart()is a string method that adds characters to the front until the text reaches a set length.pad()first turns the number into text withString(), thenpadStart(2, "0")adds zeros until it's two characters long, so 7 seconds shows as07. Text that's already two characters, like42, is left alone.// Two digits always: 7 becomes "07". function pad(number) { return String(number).padStart(2, "0"); } - 08
Turn a date into input text
A
datetime-localinput'svaluemust be text in one exact format, like2027-01-01T00:00. ADateobject hasgetmethods that return each part as a number:getFullYear(),getMonth(),getDate(),getHours()andgetMinutes().toInputValue()pads each part to two digits and joins them.getMonth()counts from 0, so January is 0 and the code adds 1.// datetime-local wants text like "2027-01-01T00:00", in local time. function toInputValue(date) { const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; const time = `${pad(date.getHours())}:${pad(date.getMinutes())}`; return `${day}T${time}`; } - 09
Split milliseconds into days, hours, minutes, and seconds
The time left is one big number of milliseconds, and the page needs it in four units. Dividing by
DAYand rounding down withMath.floor()gives the whole days.%gives the remainder after dividing, somsLeft % DAYis what's left over that doesn't make a whole day; dividing that byHOURgives the hours. Minutes and seconds follow the same pattern.function showTimeLeft(msLeft) { // Divide to get whole units; % keeps only what's left under the next unit. daysDisplay.textContent = Math.floor(msLeft / DAY); hoursDisplay.textContent = pad(Math.floor((msLeft % DAY) / HOUR)); minutesDisplay.textContent = pad(Math.floor((msLeft % HOUR) / MINUTE)); secondsDisplay.textContent = pad(Math.floor((msLeft % MINUTE) / SECOND)); } - 10
Find the time left by subtracting
To find the time left, you need both dates as plain numbers.
new Date(targetInput.value)turns the input's text into aDate, andgetTime()gives that moment in milliseconds.Date.now()gives the current moment in milliseconds. Subtracting now from the target gives the milliseconds left; if the result is zero or less, the time has passed.// How long until the picked date. getTime() turns a date into milliseconds. function timeLeft() { const target = new Date(targetInput.value); return target.getTime() - Date.now(); } - 11
Start and stop the timer
setInterval(tick, SECOND)callstick()every second and returns an id, kept intimer.clearInterval(timer)uses that id to stop it.tick()stops the timer once time runs out.start()also clears it first, before checking the new date, so picking another date never leaves two timers running.start()callstick()once straight away, so you don't wait a second for the first update.function tick() { const msLeft = timeLeft(); if (msLeft <= 0) { // Stop the timer once it reaches zero, or it would keep running. clearInterval(timer); showTimeLeft(0); statusMessage.textContent = "Time's up!"; return; } showTimeLeft(msLeft); } function start() { clearInterval(timer); statusMessage.textContent = ""; if (!targetInput.value) return; if (timeLeft() <= 0) { showTimeLeft(0); statusMessage.textContent = "Pick a time in the future."; return; } tick(); // setInterval runs tick again every second until clearInterval stops it. timer = setInterval(tick, SECOND); - 12
Make a date with
new Date()new Date(year, month, day)builds aDatefor a particular day.new Date().getFullYear()is this year, so adding 1 gives next year; month0is January, and day1is the first. That's next New Year's Day. The code puts it in the input withtoInputValue()and callsstart(), so the countdown is running as soon as the page opens.// Start by counting down to next New Year's Day. Month 0 is January. const nextNewYear = new Date(new Date().getFullYear() + 1, 0, 1); targetInput.value = toInputValue(nextNewYear); start();
03
Wrap up
Track your progress, then go deeper or keep going.
Take it further
Once it works, try these on your own. Each one uses what you've just learned, plus one new idea.
- Challenge 01
Let people name the countdown, like Birthday, and show the name in the heading.
- Challenge 02
Save the chosen date in localStorage.
- Challenge 03
Show a progress bar of how much of the wait has passed.
Learn more on the blog
Articles that go further into ideas from this project.
- How to Auto Update the Copyright Year on Your WebsiteA small first step with dates: new Date() and getFullYear() keep a footer's year current.Read the article (opens in a new tab)
- How to Build Responsive Layouts Using CSS GridGrid from the ground up: columns, areas, fr units, and minmax(), building layouts that fit any screen.Read the article (opens in a new tab)
Finished Countdown Timer?
Mark it done to track your progress.
Go deeper
The full article explains each idea in more depth, why the code is written this way, and more than fits on this page.
Article coming soon. See the series so far (opens in a new tab)