Open demo

CSS code

HTML
<div><span class="lbl">Boxed countdown</span>
<div class="cd" data-countdown><p class="sr-only">Time until launch: <span data-say></span></p>
  <div class="u" aria-hidden="true"><b data-show="d">00</b><span>Days</span></div>
  <div class="u" aria-hidden="true"><b data-show="h">00</b><span>Hours</span></div>
  <div class="u" aria-hidden="true"><b data-show="m">00</b><span>Minutes</span></div>
  <div class="u" aria-hidden="true"><b data-show="s">00</b><span>Seconds</span></div>
</div></div>
<div><span class="lbl">Inline banner counter</span>
<div class="cd-inline" data-countdown><i aria-hidden="true"></i>Launching in <b aria-hidden="true" data-show="inline">—</b><span class="sr-only" data-say></span></div></div>
CSS
/* Countdowns: the script fills [data-show] elements (d, h, m, s, inline) inside each [data-countdown] root.
   The ticking numbers are aria-hidden; [data-say] carries a minute-precision version for screen readers. */
.cd{display:flex;gap:14px}
.cd .u{background:#fff;border-radius:13px;padding:18px 20px;min-width:92px;text-align:center;box-shadow:0 2px 10px rgba(15,23,42,.1)}
.cd .u b{display:block;font-size:2.3rem;font-weight:700;line-height:1;font-variant-numeric:tabular-nums}
.cd .u span{display:block;font-size:.68rem;letter-spacing:.13em;text-transform:uppercase;color:#64748b;margin-top:7px}
.cd-inline{display:inline-flex;align-items:center;gap:10px;background:#0f172a;color:#fff;padding:11px 19px;border-radius:999px;font-size:.9rem}
.cd-inline b{font-variant-numeric:tabular-nums;letter-spacing:.03em}
.cd-inline i{width:7px;height:7px;border-radius:50%;background:#4ade80;display:block}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:520px){.cd{gap:8px}.cd .u{flex:1;min-width:0;padding:14px 4px}.cd .u b{font-size:1.8rem}.cd .u span{letter-spacing:.06em}}
JavaScript
// Countdowns. Set the launch moment here, e.g. new Date('2026-12-01T09:00:00Z').
// The demo counts to 32 days and 7 hours after the page loads, so it never reads zero.
var COUNTDOWN_TARGET = new Date(Date.now() + 32 * 864e5 + 7 * 36e5);

(function () {
  var roots = document.querySelectorAll('[data-countdown]');
  if (!roots.length) return;
  var lastSaid = '';
  function p2(n) { return String(n).padStart(2, '0'); }
  function plural(n, word) { return n + ' ' + word + (n === 1 ? '' : 's'); }
  function tick() {
    var left = Math.max(0, COUNTDOWN_TARGET - Date.now());
    var d = Math.floor(left / 864e5), h = Math.floor(left % 864e5 / 36e5),
        m = Math.floor(left % 36e5 / 6e4), s = Math.floor(left % 6e4 / 1000);
    var out = { d: p2(d), h: p2(h), m: p2(m), s: p2(s), inline: d + 'd ' + p2(h) + ':' + p2(m) + ':' + p2(s) };
    // Screen-reader text changes at most once a minute and is not a live region.
    var said = plural(d, 'day') + ', ' + plural(h, 'hour') + ' and ' + plural(m, 'minute');
    roots.forEach(function (root) {
      root.querySelectorAll('[data-show]').forEach(function (el) { el.textContent = out[el.getAttribute('data-show')]; });
      var say = root.querySelector('[data-say]');
      if (say && said !== lastSaid) say.textContent = said;
    });
    lastSaid = said;
    if (left) setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Boxed countdown</span>
<div class="cd d-flex" data-countdown><p class="visually-hidden">Time until launch: <span data-say></span></p>
  <div class="card text-center" aria-hidden="true"><div class="card-body"><b class="d-block fw-bold" data-show="d">00</b><span class="d-block text-uppercase text-body-secondary">Days</span></div></div>
  <div class="card text-center" aria-hidden="true"><div class="card-body"><b class="d-block fw-bold" data-show="h">00</b><span class="d-block text-uppercase text-body-secondary">Hours</span></div></div>
  <div class="card text-center" aria-hidden="true"><div class="card-body"><b class="d-block fw-bold" data-show="m">00</b><span class="d-block text-uppercase text-body-secondary">Minutes</span></div></div>
  <div class="card text-center" aria-hidden="true"><div class="card-body"><b class="d-block fw-bold" data-show="s">00</b><span class="d-block text-uppercase text-body-secondary">Seconds</span></div></div>
</div></div>
<div><span class="lbl">Inline banner counter</span>
<div class="cd-inline badge rounded-pill text-bg-dark d-inline-flex align-items-center" data-countdown><i class="d-block rounded-circle" aria-hidden="true"></i>Launching in <b class="fw-bold" aria-hidden="true" data-show="inline">—</b><span class="visually-hidden" data-say></span></div></div>
CSS
/* Countdowns on Bootstrap: each unit is a .card restyled through --bs-card-* variables, and the banner counter
   is a .badge.rounded-pill.text-bg-dark resized through --bs-badge-* variables. */
.cd{gap:14px}
.cd .card{--bs-card-border-width:0;--bs-card-border-radius:13px;--bs-card-spacer-y:18px;--bs-card-spacer-x:20px;
  --bs-card-color:#1e293b;--bs-secondary-color:#64748b;min-width:92px;box-shadow:0 2px 10px rgba(15,23,42,.1)}
.cd .card b{font-size:2.3rem;line-height:1;font-variant-numeric:tabular-nums}
.cd .card span{font-size:.68rem;letter-spacing:.13em;margin-top:7px}
.cd-inline{--bs-dark-rgb:15,23,42;--bs-badge-padding-x:19px;--bs-badge-padding-y:11px;--bs-badge-font-size:.9rem;
  --bs-badge-font-weight:400;gap:10px;line-height:normal}
.cd-inline b{font-variant-numeric:tabular-nums;letter-spacing:.03em}
.cd-inline i{width:7px;height:7px;background:#4ade80}
@media (max-width:520px){.cd{gap:8px}.cd .card{--bs-card-spacer-y:14px;--bs-card-spacer-x:4px;flex:1;min-width:0}.cd .card b{font-size:1.8rem}.cd .card span{letter-spacing:.06em}}
JavaScript
// Countdowns. Set the launch moment here, e.g. new Date('2026-12-01T09:00:00Z').
// The demo counts to 32 days and 7 hours after the page loads, so it never reads zero.
var COUNTDOWN_TARGET = new Date(Date.now() + 32 * 864e5 + 7 * 36e5);

(function () {
  var roots = document.querySelectorAll('[data-countdown]');
  if (!roots.length) return;
  var lastSaid = '';
  function p2(n) { return String(n).padStart(2, '0'); }
  function plural(n, word) { return n + ' ' + word + (n === 1 ? '' : 's'); }
  function tick() {
    var left = Math.max(0, COUNTDOWN_TARGET - Date.now());
    var d = Math.floor(left / 864e5), h = Math.floor(left % 864e5 / 36e5),
        m = Math.floor(left % 36e5 / 6e4), s = Math.floor(left % 6e4 / 1000);
    var out = { d: p2(d), h: p2(h), m: p2(m), s: p2(s), inline: d + 'd ' + p2(h) + ':' + p2(m) + ':' + p2(s) };
    // Screen-reader text changes at most once a minute and is not a live region.
    var said = plural(d, 'day') + ', ' + plural(h, 'hour') + ' and ' + plural(m, 'minute');
    roots.forEach(function (root) {
      root.querySelectorAll('[data-show]').forEach(function (el) { el.textContent = out[el.getAttribute('data-show')]; });
      var say = root.querySelector('[data-say]');
      if (say && said !== lastSaid) say.textContent = said;
    });
    lastSaid = said;
    if (left) setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Boxed countdown</span>
<div class="flex gap-2 min-[521px]:gap-3.5" data-countdown><p class="sr-only">Time until launch: <span data-say></span></p>
  <div class="min-w-0 flex-1 rounded-[13px] bg-white px-1 py-3.5 text-center shadow-[0_2px_10px_rgba(15,23,42,.1)] min-[521px]:min-w-[92px] min-[521px]:flex-none min-[521px]:px-5 min-[521px]:py-[18px]" aria-hidden="true"><b class="block text-[1.8rem] leading-none font-bold tabular-nums min-[521px]:text-[2.3rem]" data-show="d">00</b><span class="mt-[7px] block text-[.68rem] tracking-[.06em] text-[#64748b] uppercase min-[521px]:tracking-[.13em]">Days</span></div>
  <div class="min-w-0 flex-1 rounded-[13px] bg-white px-1 py-3.5 text-center shadow-[0_2px_10px_rgba(15,23,42,.1)] min-[521px]:min-w-[92px] min-[521px]:flex-none min-[521px]:px-5 min-[521px]:py-[18px]" aria-hidden="true"><b class="block text-[1.8rem] leading-none font-bold tabular-nums min-[521px]:text-[2.3rem]" data-show="h">00</b><span class="mt-[7px] block text-[.68rem] tracking-[.06em] text-[#64748b] uppercase min-[521px]:tracking-[.13em]">Hours</span></div>
  <div class="min-w-0 flex-1 rounded-[13px] bg-white px-1 py-3.5 text-center shadow-[0_2px_10px_rgba(15,23,42,.1)] min-[521px]:min-w-[92px] min-[521px]:flex-none min-[521px]:px-5 min-[521px]:py-[18px]" aria-hidden="true"><b class="block text-[1.8rem] leading-none font-bold tabular-nums min-[521px]:text-[2.3rem]" data-show="m">00</b><span class="mt-[7px] block text-[.68rem] tracking-[.06em] text-[#64748b] uppercase min-[521px]:tracking-[.13em]">Minutes</span></div>
  <div class="min-w-0 flex-1 rounded-[13px] bg-white px-1 py-3.5 text-center shadow-[0_2px_10px_rgba(15,23,42,.1)] min-[521px]:min-w-[92px] min-[521px]:flex-none min-[521px]:px-5 min-[521px]:py-[18px]" aria-hidden="true"><b class="block text-[1.8rem] leading-none font-bold tabular-nums min-[521px]:text-[2.3rem]" data-show="s">00</b><span class="mt-[7px] block text-[.68rem] tracking-[.06em] text-[#64748b] uppercase min-[521px]:tracking-[.13em]">Seconds</span></div>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Inline banner counter</span>
<div class="inline-flex items-center gap-2.5 rounded-full bg-[#0f172a] px-[19px] py-[11px] text-[.9rem] text-white" data-countdown><i class="block size-[7px] rounded-full bg-[#4ade80]" aria-hidden="true"></i>Launching in <b class="tracking-[.03em] tabular-nums" aria-hidden="true" data-show="inline">—</b><span class="sr-only" data-say></span></div></div>
JavaScript
// Countdowns. Set the launch moment here, e.g. new Date('2026-12-01T09:00:00Z').
// The demo counts to 32 days and 7 hours after the page loads, so it never reads zero.
var COUNTDOWN_TARGET = new Date(Date.now() + 32 * 864e5 + 7 * 36e5);

(function () {
  var roots = document.querySelectorAll('[data-countdown]');
  if (!roots.length) return;
  var lastSaid = '';
  function p2(n) { return String(n).padStart(2, '0'); }
  function plural(n, word) { return n + ' ' + word + (n === 1 ? '' : 's'); }
  function tick() {
    var left = Math.max(0, COUNTDOWN_TARGET - Date.now());
    var d = Math.floor(left / 864e5), h = Math.floor(left % 864e5 / 36e5),
        m = Math.floor(left % 36e5 / 6e4), s = Math.floor(left % 6e4 / 1000);
    var out = { d: p2(d), h: p2(h), m: p2(m), s: p2(s), inline: d + 'd ' + p2(h) + ':' + p2(m) + ':' + p2(s) };
    // Screen-reader text changes at most once a minute and is not a live region.
    var said = plural(d, 'day') + ', ' + plural(h, 'hour') + ' and ' + plural(m, 'minute');
    roots.forEach(function (root) {
      root.querySelectorAll('[data-show]').forEach(function (el) { el.textContent = out[el.getAttribute('data-show')]; });
      var say = root.querySelector('[data-say]');
      if (say && said !== lastSaid) say.textContent = said;
    });
    lastSaid = said;
    if (left) setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

About this set

Two countdowns to a launch moment. The boxed countdown shows days, hours, minutes and seconds in four white tiles with uppercase unit labels, for the hero of a launch, event or sale page. The inline banner counter is a dark pill with a green status dot that reads Launching in 32d 06:59:59, for an announcement bar or a header where space is short. The target is one line at the top of the script, a Date you can set to any fixed moment; the demo counts to 32 days and 7 hours after the page loads so it never reads zero. The remaining time is always the target minus Date.now(), so it stays correct in a background tab, and the loop stops when it reaches zero. Values land in elements marked with data-show, so both styles can sit on one page and share one script. Below 520 pixels the four tiles share the width equally. The Bootstrap version builds the tiles from .card and the banner from .badge.rounded-pill, and the Tailwind version uses utility classes only.

What’s included

  • Four-tile days, hours, minutes and seconds countdown
  • Compact pill counter for a banner or header
  • Target date set on one line at the top of the script
  • Time left computed from Date.now(), so throttled tabs stay accurate
  • Tiles share the width equally on small screens

Accessibility

The ticking numbers are aria-hidden, and visually hidden text such as "Time until launch: 32 days, 6 hours and 59 minutes" changes once a minute and is not a live region, so screen readers are not interrupted every second. The unit labels were darkened to reach 4.5:1 against the white tiles. Nothing animates.

Customise it

Set COUNTDOWN_TARGET at the top of the script. In plain CSS change the colours and sizes on .cd .u and .cd-inline; in Bootstrap the --bs-card-* variables on .cd .card and the --bs-badge-* variables on .cd-inline; in Tailwind the bg-, text- and rounded- utilities.