Countdown timer
Category: Data display · Tags: countdown, timer, deadline, time, live-region
Early-bird pricing ends in
Ends .
How to use it
Use for a real, fixed deadline such as the end of early-bird pricing or a registration window. Do not use it to invent urgency or for durations under a minute; for a short wait use a progress indicator. Set data-target and the <time> element to the same ISO 8601 instant with an explicit timezone, and change the ended message in the script.
Accessibility
The deadline is always written as text in a <time datetime> element, so it makes sense without scripts. The ticking day, hour, minute and second boxes are aria-hidden, and a visually hidden role="timer" line instead holds, updated each minute and not announced automatically, "30 days, 4 hours and 12 minutes left" only when the minute changes, so screen readers are not interrupted every second. Nothing animates (numbers simply change), so it is safe with reduced motion. Numbers are white on #0f172a (17.85:1), unit labels and the deadline #334155 on white (10.35:1), and the heading #0f172a on white (17.85:1).
Source (copy-ready)
<section class="dcl-countdown" aria-labelledby="dcl-countdown-title" data-target="2027-01-01T00:00:00Z">
<h2 class="dcl-countdown__title" id="dcl-countdown-title">Early-bird pricing ends in</h2>
<div class="dcl-countdown__units" aria-hidden="true">
<div class="dcl-countdown__unit"><span class="dcl-countdown__number" data-unit="days">--</span><span class="dcl-countdown__label">days</span></div>
<div class="dcl-countdown__unit"><span class="dcl-countdown__number" data-unit="hours">--</span><span class="dcl-countdown__label">hours</span></div>
<div class="dcl-countdown__unit"><span class="dcl-countdown__number" data-unit="minutes">--</span><span class="dcl-countdown__label">minutes</span></div>
<div class="dcl-countdown__unit"><span class="dcl-countdown__number" data-unit="seconds">--</span><span class="dcl-countdown__label">seconds</span></div>
</div>
<p class="dcl-countdown__sr" role="timer"></p>
<p class="dcl-countdown__deadline">Ends <time datetime="2027-01-01T00:00:00Z">1 January 2027 at 00:00 UTC</time>.</p>
</section>
<style>
.dcl-countdown { max-width: 30rem; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-countdown__title { margin: 0 0 0.6rem; font-size: 1.1rem; color: #0f172a; background-color: #ffffff; }
.dcl-countdown__units { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.dcl-countdown__unit { display: flex; flex-direction: column; align-items: center; flex: 1 1 4rem; min-width: 0; color: #334155; background-color: #ffffff; }
.dcl-countdown__number { box-sizing: border-box; width: 100%; padding: 0.5rem 0.25rem; border-radius: 6px; font-size: 1.75rem; font-weight: 700; font-variant-numeric: tabular-nums; text-align: center; color: #ffffff; background-color: #0f172a; }
.dcl-countdown__label { margin-top: 0.25rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; color: #334155; background-color: #ffffff; }
.dcl-countdown__deadline { margin: 0.75rem 0 0; font-size: 0.9rem; color: #334155; background-color: #ffffff; }
.dcl-countdown__sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
</style>
<script>
(function () {
function plural(n, word) { return n + " " + word + (n === 1 ? "" : "s"); }
document.querySelectorAll(".dcl-countdown").forEach(function (root) {
if (root.dataset.dclBound) { return; }
root.dataset.dclBound = "true";
var target = Date.parse(root.dataset.target || "");
var live = root.querySelector(".dcl-countdown__sr");
if (isNaN(target)) { return; }
var fields = {};
root.querySelectorAll("[data-unit]").forEach(function (node) { fields[node.dataset.unit] = node; });
var lastSpoken = "";
var timer = null;
function set(unit, value, pad) {
if (fields[unit]) { fields[unit].textContent = pad ? String(value).padStart(2, "0") : String(value); }
}
function tick() {
var left = Math.max(0, Math.floor((target - Date.now()) / 1000));
var days = Math.floor(left / 86400);
var hours = Math.floor((left % 86400) / 3600);
var minutes = Math.floor((left % 3600) / 60);
set("days", days, false);
set("hours", hours, true);
set("minutes", minutes, true);
set("seconds", left % 60, true);
var spoken = left === 0 ? "Early-bird pricing has ended." :
plural(days, "day") + ", " + plural(hours, "hour") + " and " + plural(minutes, "minute") + " left.";
if (live && spoken !== lastSpoken) {
lastSpoken = spoken;
live.textContent = spoken;
}
if (left === 0 && timer) { clearInterval(timer); }
}
tick();
if (target > Date.now()) { timer = setInterval(tick, 1000); }
});
})();
</script>
Was this useful? Send feedback.