Product Launch Countdown Timer
A launch countdown that survives contact with reality: it computes from a real target timestamp (not a decrementing variable, which drifts and breaks on tab sleep), renders days/hours/minutes/seconds with progress rings, announces at meaningful thresholds only, and degrades to a live 'We're live' state when it reaches zero.
Published Updated
The code
<section class="cnc-13" aria-label="Product launch countdown timer demo">
<div class="cnc-13__stage">
<div class="cnc-13__panel" data-state="live">
<p class="cnc-13__eyebrow">Public beta opens</p>
<h2 class="cnc-13__head">Halo Studio 3.0</h2>
<div class="cnc-13__units" role="timer" aria-label="Time remaining until launch">
<div class="cnc-13__unit" data-max="30" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">days</span></div>
<div class="cnc-13__unit" data-max="24" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">hours</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">minutes</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">seconds</span></div>
</div>
<p class="cnc-13__when"></p>
<button class="cnc-13__cta" type="button">Notify me at launch</button>
<p class="cnc-13__sr" role="status" aria-live="polite"></p>
</div>
<p class="cnc-13__chip" aria-hidden="true">recompute from timestamp · conic ring per unit · threshold-only announcements</p>
</div>
</section><section class="cnc-13" aria-label="Product launch countdown timer demo">
<div class="cnc-13__stage">
<div class="cnc-13__panel" data-state="live">
<p class="cnc-13__eyebrow">Public beta opens</p>
<h2 class="cnc-13__head">Halo Studio 3.0</h2>
<div class="cnc-13__units" role="timer" aria-label="Time remaining until launch">
<div class="cnc-13__unit" data-max="30" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">days</span></div>
<div class="cnc-13__unit" data-max="24" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">hours</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">minutes</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">seconds</span></div>
</div>
<p class="cnc-13__when"></p>
<button class="cnc-13__cta" type="button">Notify me at launch</button>
<p class="cnc-13__sr" role="status" aria-live="polite"></p>
</div>
<p class="cnc-13__chip" aria-hidden="true">recompute from timestamp · conic ring per unit · threshold-only announcements</p>
</div>
</section>.cnc-13,
.cnc-13 *,
.cnc-13 *::before,
.cnc-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cnc-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--cnc-13-bg: oklch(0.17 0.05 285);
--cnc-13-ink: oklch(0.98 0.005 280);
--cnc-13-mut: oklch(0.72 0.03 285);
--cnc-13-acc: oklch(0.78 0.18 195);
--cnc-13-acc2: oklch(0.66 0.22 305);
background: var(--cnc-13-bg);
color: var(--cnc-13-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}
.cnc-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 20px;
padding: clamp(18px,4vw,48px);
background: radial-gradient(70% 60% at 20% 0%,color-mix(in oklch,var(--cnc-13-acc2) 30%,transparent),transparent 65%),radial-gradient(60% 55% at 85% 100%,color-mix(in oklch,var(--cnc-13-acc) 25%,transparent),transparent 65%);
}
.cnc-13__panel {
width: min(100%,600px);
padding: clamp(24px,4.5vw,38px);
border-radius: 26px;
text-align: center;
background: oklch(1 0 0/.06);
border: 1px solid oklch(1 0 0/.13);
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
box-shadow: 0 30px 70px oklch(0 0 0/.4);
}
.cnc-13__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--cnc-13-acc);
}
.cnc-13__head {
margin-top: 8px;
font-size: clamp(26px,4.6vw,38px);
font-weight: 700;
letter-spacing: -.035em;
}
.cnc-13__units {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(8px,2vw,16px);
margin-top: 26px;
}
.cnc-13__unit {
display: grid;
justify-items: center;
gap: 9px;
}
.cnc-13__ring {
position: relative;
display: grid;
place-items: center;
width: 100%;
max-width: 104px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--cnc-13-acc) calc(var(--cnc-13-p,0) * 360deg),oklch(1 0 0/.09) 0);
transition: background .35s linear;
}
.cnc-13__ring::before {
content: "";
position: absolute;
inset: 7px;
border-radius: 50%;
background: color-mix(in oklch,var(--cnc-13-bg) 88%,black);
}
.cnc-13__n {
position: relative;
font-size: clamp(22px,4.6vw,32px);
font-weight: 750;
letter-spacing: -.04em;
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1;
}
.cnc-13__unit.is-tick .cnc-13__n {
animation: cnc-13-tick .4s cubic-bezier(.16,1,.3,1);
}
@keyframes cnc-13-tick {
0% {
opacity: .25;
translate: 0 6px;
}
100% {
opacity: 1;
translate: 0 0;
}
}
.cnc-13__u {
font-size: 10.5px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--cnc-13-mut);
}
.cnc-13__when {
margin-top: 22px;
font-size: 12.5px;
color: var(--cnc-13-mut);
}
.cnc-13__cta {
min-height: 48px;
margin-top: 18px;
padding: 0 26px;
border: 0;
border-radius: 99px;
background: linear-gradient(100deg,var(--cnc-13-acc),var(--cnc-13-acc2));
color: oklch(0.16 0.04 280);
font: inherit;
font-size: 14.5px;
font-weight: 700;
cursor: pointer;
transition: transform .2s,filter .2s;
}
.cnc-13__cta:hover {
transform: translateY(-2px);
filter: brightness(1.08);
}
.cnc-13__cta:focus-visible {
outline: 3px solid var(--cnc-13-ink);
outline-offset: 3px;
}
.cnc-13__panel[data-state="done"] .cnc-13__units {
opacity: .35;
}
.cnc-13__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.cnc-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cnc-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
max-width: min(100%,540px);
}
@media (max-width: 420px) {
.cnc-13__units {
grid-template-columns: repeat(2,1fr);
}
}
@media (prefers-reduced-motion: reduce) {
.cnc-13 * {
animation: none !important;
transition-duration: .01ms !important;
}
}.cnc-13,
.cnc-13 *,
.cnc-13 *::before,
.cnc-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cnc-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--cnc-13-bg: oklch(0.17 0.05 285);
--cnc-13-ink: oklch(0.98 0.005 280);
--cnc-13-mut: oklch(0.72 0.03 285);
--cnc-13-acc: oklch(0.78 0.18 195);
--cnc-13-acc2: oklch(0.66 0.22 305);
background: var(--cnc-13-bg);
color: var(--cnc-13-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}
.cnc-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 20px;
padding: clamp(18px,4vw,48px);
background: radial-gradient(70% 60% at 20% 0%,color-mix(in oklch,var(--cnc-13-acc2) 30%,transparent),transparent 65%),radial-gradient(60% 55% at 85% 100%,color-mix(in oklch,var(--cnc-13-acc) 25%,transparent),transparent 65%);
}
.cnc-13__panel {
width: min(100%,600px);
padding: clamp(24px,4.5vw,38px);
border-radius: 26px;
text-align: center;
background: oklch(1 0 0/.06);
border: 1px solid oklch(1 0 0/.13);
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
box-shadow: 0 30px 70px oklch(0 0 0/.4);
}
.cnc-13__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--cnc-13-acc);
}
.cnc-13__head {
margin-top: 8px;
font-size: clamp(26px,4.6vw,38px);
font-weight: 700;
letter-spacing: -.035em;
}
.cnc-13__units {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(8px,2vw,16px);
margin-top: 26px;
}
.cnc-13__unit {
display: grid;
justify-items: center;
gap: 9px;
}
.cnc-13__ring {
position: relative;
display: grid;
place-items: center;
width: 100%;
max-width: 104px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--cnc-13-acc) calc(var(--cnc-13-p,0) * 360deg),oklch(1 0 0/.09) 0);
transition: background .35s linear;
}
.cnc-13__ring::before {
content: "";
position: absolute;
inset: 7px;
border-radius: 50%;
background: color-mix(in oklch,var(--cnc-13-bg) 88%,black);
}
.cnc-13__n {
position: relative;
font-size: clamp(22px,4.6vw,32px);
font-weight: 750;
letter-spacing: -.04em;
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1;
}
.cnc-13__unit.is-tick .cnc-13__n {
animation: cnc-13-tick .4s cubic-bezier(.16,1,.3,1);
}
@keyframes cnc-13-tick {
0% {
opacity: .25;
translate: 0 6px;
}
100% {
opacity: 1;
translate: 0 0;
}
}
.cnc-13__u {
font-size: 10.5px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--cnc-13-mut);
}
.cnc-13__when {
margin-top: 22px;
font-size: 12.5px;
color: var(--cnc-13-mut);
}
.cnc-13__cta {
min-height: 48px;
margin-top: 18px;
padding: 0 26px;
border: 0;
border-radius: 99px;
background: linear-gradient(100deg,var(--cnc-13-acc),var(--cnc-13-acc2));
color: oklch(0.16 0.04 280);
font: inherit;
font-size: 14.5px;
font-weight: 700;
cursor: pointer;
transition: transform .2s,filter .2s;
}
.cnc-13__cta:hover {
transform: translateY(-2px);
filter: brightness(1.08);
}
.cnc-13__cta:focus-visible {
outline: 3px solid var(--cnc-13-ink);
outline-offset: 3px;
}
.cnc-13__panel[data-state="done"] .cnc-13__units {
opacity: .35;
}
.cnc-13__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.cnc-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cnc-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
max-width: min(100%,540px);
}
@media (max-width: 420px) {
.cnc-13__units {
grid-template-columns: repeat(2,1fr);
}
}
@media (prefers-reduced-motion: reduce) {
.cnc-13 * {
animation: none !important;
transition-duration: .01ms !important;
}
}(() => {
document.querySelectorAll('.cnc-13__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const units = [...panel.querySelectorAll('.cnc-13__unit')];
const whenEl = panel.querySelector('.cnc-13__when');
const say = panel.querySelector('.cnc-13__sr');
// Fixed launch date; rolled forward if it has already passed so the demo never sits at zero.
let target = Date.parse('2026-11-18T15:00:00Z');
while (target < Date.now()) target += 27 * 864e5;
whenEl.textContent = new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'short' }).format(target);
const thresholds = [86400, 3600, 600, 60, 0];
let spoken = new Set();
const set = (unit, val, frac) => {
const b = unit.querySelector('.cnc-13__n');
const txt = String(val).padStart(2, '0');
unit.style.setProperty('--cnc-13-p', frac.toFixed(4));
if (b.textContent === txt) return;
b.textContent = txt;
unit.classList.remove('is-tick'); void unit.offsetWidth; unit.classList.add('is-tick');
};
const tick = () => {
const ms = Math.max(0, target - Date.now()); // always recomputed, never decremented
const total = Math.floor(ms / 1000);
const d = Math.floor(total / 86400), h = Math.floor(total / 3600) % 24, m = Math.floor(total / 60) % 60, s = total % 60;
set(units[0], d, Math.min(1, d / 30));
set(units[1], h, h / 24);
set(units[2], m, m / 60);
set(units[3], s, s / 60);
panel.dataset.state = total === 0 ? 'done' : 'live';
const hit = thresholds.find((t) => total <= t && !spoken.has(t));
if (hit !== undefined) {
spoken.add(hit);
say.textContent = total === 0 ? 'We are live' : d + ' days ' + h + ' hours ' + m + ' minutes remaining until launch';
}
};
tick();
const id = setInterval(tick, 1000);
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') tick(); });
panel.querySelector('.cnc-13__cta').addEventListener('click', (e) => {
e.currentTarget.textContent = 'You are on the list ✓';
say.textContent = 'You will be notified at launch';
});
addEventListener('pagehide', () => clearInterval(id), { once: true });
});
})();(() => {
document.querySelectorAll('.cnc-13__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const units = [...panel.querySelectorAll('.cnc-13__unit')];
const whenEl = panel.querySelector('.cnc-13__when');
const say = panel.querySelector('.cnc-13__sr');
// Fixed launch date; rolled forward if it has already passed so the demo never sits at zero.
let target = Date.parse('2026-11-18T15:00:00Z');
while (target < Date.now()) target += 27 * 864e5;
whenEl.textContent = new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'short' }).format(target);
const thresholds = [86400, 3600, 600, 60, 0];
let spoken = new Set();
const set = (unit, val, frac) => {
const b = unit.querySelector('.cnc-13__n');
const txt = String(val).padStart(2, '0');
unit.style.setProperty('--cnc-13-p', frac.toFixed(4));
if (b.textContent === txt) return;
b.textContent = txt;
unit.classList.remove('is-tick'); void unit.offsetWidth; unit.classList.add('is-tick');
};
const tick = () => {
const ms = Math.max(0, target - Date.now()); // always recomputed, never decremented
const total = Math.floor(ms / 1000);
const d = Math.floor(total / 86400), h = Math.floor(total / 3600) % 24, m = Math.floor(total / 60) % 60, s = total % 60;
set(units[0], d, Math.min(1, d / 30));
set(units[1], h, h / 24);
set(units[2], m, m / 60);
set(units[3], s, s / 60);
panel.dataset.state = total === 0 ? 'done' : 'live';
const hit = thresholds.find((t) => total <= t && !spoken.has(t));
if (hit !== undefined) {
spoken.add(hit);
say.textContent = total === 0 ? 'We are live' : d + ' days ' + h + ' hours ' + m + ' minutes remaining until launch';
}
};
tick();
const id = setInterval(tick, 1000);
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') tick(); });
panel.querySelector('.cnc-13__cta').addEventListener('click', (e) => {
e.currentTarget.textContent = 'You are on the list ✓';
say.textContent = 'You will be notified at launch';
});
addEventListener('pagehide', () => clearInterval(id), { once: true });
});
})();Here's a working CSS Number Counter Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Product Launch Countdown Timer
Source: https://codefronts.com/motion/css-number-counter-animations/product-launch-countdown/
A launch countdown that survives contact with reality: it computes from a real target timestamp (not a decrementing variable, which drifts and breaks on tab sleep), renders days/hours/minutes/seconds with progress rings, announces at meaningful thresholds only, and degrades to a live 'We're live' state when it reaches zero.
## HTML
```html
<section class="cnc-13" aria-label="Product launch countdown timer demo">
<div class="cnc-13__stage">
<div class="cnc-13__panel" data-state="live">
<p class="cnc-13__eyebrow">Public beta opens</p>
<h2 class="cnc-13__head">Halo Studio 3.0</h2>
<div class="cnc-13__units" role="timer" aria-label="Time remaining until launch">
<div class="cnc-13__unit" data-max="30" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">days</span></div>
<div class="cnc-13__unit" data-max="24" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">hours</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">minutes</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">seconds</span></div>
</div>
<p class="cnc-13__when"></p>
<button class="cnc-13__cta" type="button">Notify me at launch</button>
<p class="cnc-13__sr" role="status" aria-live="polite"></p>
</div>
<p class="cnc-13__chip" aria-hidden="true">recompute from timestamp · conic ring per unit · threshold-only announcements</p>
</div>
</section>
```
## CSS
```css
.cnc-13,
.cnc-13 *,
.cnc-13 *::before,
.cnc-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cnc-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--cnc-13-bg: oklch(0.17 0.05 285);
--cnc-13-ink: oklch(0.98 0.005 280);
--cnc-13-mut: oklch(0.72 0.03 285);
--cnc-13-acc: oklch(0.78 0.18 195);
--cnc-13-acc2: oklch(0.66 0.22 305);
background: var(--cnc-13-bg);
color: var(--cnc-13-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}
.cnc-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 20px;
padding: clamp(18px,4vw,48px);
background: radial-gradient(70% 60% at 20% 0%,color-mix(in oklch,var(--cnc-13-acc2) 30%,transparent),transparent 65%),radial-gradient(60% 55% at 85% 100%,color-mix(in oklch,var(--cnc-13-acc) 25%,transparent),transparent 65%);
}
.cnc-13__panel {
width: min(100%,600px);
padding: clamp(24px,4.5vw,38px);
border-radius: 26px;
text-align: center;
background: oklch(1 0 0/.06);
border: 1px solid oklch(1 0 0/.13);
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
box-shadow: 0 30px 70px oklch(0 0 0/.4);
}
.cnc-13__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--cnc-13-acc);
}
.cnc-13__head {
margin-top: 8px;
font-size: clamp(26px,4.6vw,38px);
font-weight: 700;
letter-spacing: -.035em;
}
.cnc-13__units {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(8px,2vw,16px);
margin-top: 26px;
}
.cnc-13__unit {
display: grid;
justify-items: center;
gap: 9px;
}
.cnc-13__ring {
position: relative;
display: grid;
place-items: center;
width: 100%;
max-width: 104px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--cnc-13-acc) calc(var(--cnc-13-p,0) * 360deg),oklch(1 0 0/.09) 0);
transition: background .35s linear;
}
.cnc-13__ring::before {
content: "";
position: absolute;
inset: 7px;
border-radius: 50%;
background: color-mix(in oklch,var(--cnc-13-bg) 88%,black);
}
.cnc-13__n {
position: relative;
font-size: clamp(22px,4.6vw,32px);
font-weight: 750;
letter-spacing: -.04em;
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1;
}
.cnc-13__unit.is-tick .cnc-13__n {
animation: cnc-13-tick .4s cubic-bezier(.16,1,.3,1);
}
@keyframes cnc-13-tick {
0% {
opacity: .25;
translate: 0 6px;
}
100% {
opacity: 1;
translate: 0 0;
}
}
.cnc-13__u {
font-size: 10.5px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--cnc-13-mut);
}
.cnc-13__when {
margin-top: 22px;
font-size: 12.5px;
color: var(--cnc-13-mut);
}
.cnc-13__cta {
min-height: 48px;
margin-top: 18px;
padding: 0 26px;
border: 0;
border-radius: 99px;
background: linear-gradient(100deg,var(--cnc-13-acc),var(--cnc-13-acc2));
color: oklch(0.16 0.04 280);
font: inherit;
font-size: 14.5px;
font-weight: 700;
cursor: pointer;
transition: transform .2s,filter .2s;
}
.cnc-13__cta:hover {
transform: translateY(-2px);
filter: brightness(1.08);
}
.cnc-13__cta:focus-visible {
outline: 3px solid var(--cnc-13-ink);
outline-offset: 3px;
}
.cnc-13__panel[data-state="done"] .cnc-13__units {
opacity: .35;
}
.cnc-13__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.cnc-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cnc-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
max-width: min(100%,540px);
}
@media (max-width: 420px) {
.cnc-13__units {
grid-template-columns: repeat(2,1fr);
}
}
@media (prefers-reduced-motion: reduce) {
.cnc-13 * {
animation: none !important;
transition-duration: .01ms !important;
}
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('.cnc-13__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const units = [...panel.querySelectorAll('.cnc-13__unit')];
const whenEl = panel.querySelector('.cnc-13__when');
const say = panel.querySelector('.cnc-13__sr');
// Fixed launch date; rolled forward if it has already passed so the demo never sits at zero.
let target = Date.parse('2026-11-18T15:00:00Z');
while (target < Date.now()) target += 27 * 864e5;
whenEl.textContent = new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'short' }).format(target);
const thresholds = [86400, 3600, 600, 60, 0];
let spoken = new Set();
const set = (unit, val, frac) => {
const b = unit.querySelector('.cnc-13__n');
const txt = String(val).padStart(2, '0');
unit.style.setProperty('--cnc-13-p', frac.toFixed(4));
if (b.textContent === txt) return;
b.textContent = txt;
unit.classList.remove('is-tick'); void unit.offsetWidth; unit.classList.add('is-tick');
};
const tick = () => {
const ms = Math.max(0, target - Date.now()); // always recomputed, never decremented
const total = Math.floor(ms / 1000);
const d = Math.floor(total / 86400), h = Math.floor(total / 3600) % 24, m = Math.floor(total / 60) % 60, s = total % 60;
set(units[0], d, Math.min(1, d / 30));
set(units[1], h, h / 24);
set(units[2], m, m / 60);
set(units[3], s, s / 60);
panel.dataset.state = total === 0 ? 'done' : 'live';
const hit = thresholds.find((t) => total <= t && !spoken.has(t));
if (hit !== undefined) {
spoken.add(hit);
say.textContent = total === 0 ? 'We are live' : d + ' days ' + h + ' hours ' + m + ' minutes remaining until launch';
}
};
tick();
const id = setInterval(tick, 1000);
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') tick(); });
panel.querySelector('.cnc-13__cta').addEventListener('click', (e) => {
e.currentTarget.textContent = 'You are on the list ✓';
say.textContent = 'You will be notified at launch';
});
addEventListener('pagehide', () => clearInterval(id), { once: true });
});
})();
```Here's a working CSS Number Counter Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Product Launch Countdown Timer
Source: https://codefronts.com/motion/css-number-counter-animations/product-launch-countdown/
A launch countdown that survives contact with reality: it computes from a real target timestamp (not a decrementing variable, which drifts and breaks on tab sleep), renders days/hours/minutes/seconds with progress rings, announces at meaningful thresholds only, and degrades to a live 'We're live' state when it reaches zero.
## HTML
```html
<section class="cnc-13" aria-label="Product launch countdown timer demo">
<div class="cnc-13__stage">
<div class="cnc-13__panel" data-state="live">
<p class="cnc-13__eyebrow">Public beta opens</p>
<h2 class="cnc-13__head">Halo Studio 3.0</h2>
<div class="cnc-13__units" role="timer" aria-label="Time remaining until launch">
<div class="cnc-13__unit" data-max="30" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">days</span></div>
<div class="cnc-13__unit" data-max="24" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">hours</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">minutes</span></div>
<div class="cnc-13__unit" data-max="60" style="--cnc-13-p:0"><span class="cnc-13__ring"><b class="cnc-13__n">00</b></span><span class="cnc-13__u">seconds</span></div>
</div>
<p class="cnc-13__when"></p>
<button class="cnc-13__cta" type="button">Notify me at launch</button>
<p class="cnc-13__sr" role="status" aria-live="polite"></p>
</div>
<p class="cnc-13__chip" aria-hidden="true">recompute from timestamp · conic ring per unit · threshold-only announcements</p>
</div>
</section>
```
## CSS
```css
.cnc-13,
.cnc-13 *,
.cnc-13 *::before,
.cnc-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cnc-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
--cnc-13-bg: oklch(0.17 0.05 285);
--cnc-13-ink: oklch(0.98 0.005 280);
--cnc-13-mut: oklch(0.72 0.03 285);
--cnc-13-acc: oklch(0.78 0.18 195);
--cnc-13-acc2: oklch(0.66 0.22 305);
background: var(--cnc-13-bg);
color: var(--cnc-13-ink);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}
.cnc-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 20px;
padding: clamp(18px,4vw,48px);
background: radial-gradient(70% 60% at 20% 0%,color-mix(in oklch,var(--cnc-13-acc2) 30%,transparent),transparent 65%),radial-gradient(60% 55% at 85% 100%,color-mix(in oklch,var(--cnc-13-acc) 25%,transparent),transparent 65%);
}
.cnc-13__panel {
width: min(100%,600px);
padding: clamp(24px,4.5vw,38px);
border-radius: 26px;
text-align: center;
background: oklch(1 0 0/.06);
border: 1px solid oklch(1 0 0/.13);
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
box-shadow: 0 30px 70px oklch(0 0 0/.4);
}
.cnc-13__eyebrow {
font-size: 11.5px;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--cnc-13-acc);
}
.cnc-13__head {
margin-top: 8px;
font-size: clamp(26px,4.6vw,38px);
font-weight: 700;
letter-spacing: -.035em;
}
.cnc-13__units {
display: grid;
grid-template-columns: repeat(4,1fr);
gap: clamp(8px,2vw,16px);
margin-top: 26px;
}
.cnc-13__unit {
display: grid;
justify-items: center;
gap: 9px;
}
.cnc-13__ring {
position: relative;
display: grid;
place-items: center;
width: 100%;
max-width: 104px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--cnc-13-acc) calc(var(--cnc-13-p,0) * 360deg),oklch(1 0 0/.09) 0);
transition: background .35s linear;
}
.cnc-13__ring::before {
content: "";
position: absolute;
inset: 7px;
border-radius: 50%;
background: color-mix(in oklch,var(--cnc-13-bg) 88%,black);
}
.cnc-13__n {
position: relative;
font-size: clamp(22px,4.6vw,32px);
font-weight: 750;
letter-spacing: -.04em;
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1;
}
.cnc-13__unit.is-tick .cnc-13__n {
animation: cnc-13-tick .4s cubic-bezier(.16,1,.3,1);
}
@keyframes cnc-13-tick {
0% {
opacity: .25;
translate: 0 6px;
}
100% {
opacity: 1;
translate: 0 0;
}
}
.cnc-13__u {
font-size: 10.5px;
font-weight: 700;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--cnc-13-mut);
}
.cnc-13__when {
margin-top: 22px;
font-size: 12.5px;
color: var(--cnc-13-mut);
}
.cnc-13__cta {
min-height: 48px;
margin-top: 18px;
padding: 0 26px;
border: 0;
border-radius: 99px;
background: linear-gradient(100deg,var(--cnc-13-acc),var(--cnc-13-acc2));
color: oklch(0.16 0.04 280);
font: inherit;
font-size: 14.5px;
font-weight: 700;
cursor: pointer;
transition: transform .2s,filter .2s;
}
.cnc-13__cta:hover {
transform: translateY(-2px);
filter: brightness(1.08);
}
.cnc-13__cta:focus-visible {
outline: 3px solid var(--cnc-13-ink);
outline-offset: 3px;
}
.cnc-13__panel[data-state="done"] .cnc-13__units {
opacity: .35;
}
.cnc-13__sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.cnc-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--cnc-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
max-width: min(100%,540px);
}
@media (max-width: 420px) {
.cnc-13__units {
grid-template-columns: repeat(2,1fr);
}
}
@media (prefers-reduced-motion: reduce) {
.cnc-13 * {
animation: none !important;
transition-duration: .01ms !important;
}
}
```
## JavaScript
```js
(() => {
document.querySelectorAll('.cnc-13__panel').forEach((panel) => {
if (panel.dataset.wired) return;
panel.dataset.wired = '1';
const units = [...panel.querySelectorAll('.cnc-13__unit')];
const whenEl = panel.querySelector('.cnc-13__when');
const say = panel.querySelector('.cnc-13__sr');
// Fixed launch date; rolled forward if it has already passed so the demo never sits at zero.
let target = Date.parse('2026-11-18T15:00:00Z');
while (target < Date.now()) target += 27 * 864e5;
whenEl.textContent = new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'short' }).format(target);
const thresholds = [86400, 3600, 600, 60, 0];
let spoken = new Set();
const set = (unit, val, frac) => {
const b = unit.querySelector('.cnc-13__n');
const txt = String(val).padStart(2, '0');
unit.style.setProperty('--cnc-13-p', frac.toFixed(4));
if (b.textContent === txt) return;
b.textContent = txt;
unit.classList.remove('is-tick'); void unit.offsetWidth; unit.classList.add('is-tick');
};
const tick = () => {
const ms = Math.max(0, target - Date.now()); // always recomputed, never decremented
const total = Math.floor(ms / 1000);
const d = Math.floor(total / 86400), h = Math.floor(total / 3600) % 24, m = Math.floor(total / 60) % 60, s = total % 60;
set(units[0], d, Math.min(1, d / 30));
set(units[1], h, h / 24);
set(units[2], m, m / 60);
set(units[3], s, s / 60);
panel.dataset.state = total === 0 ? 'done' : 'live';
const hit = thresholds.find((t) => total <= t && !spoken.has(t));
if (hit !== undefined) {
spoken.add(hit);
say.textContent = total === 0 ? 'We are live' : d + ' days ' + h + ' hours ' + m + ' minutes remaining until launch';
}
};
tick();
const id = setInterval(tick, 1000);
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') tick(); });
panel.querySelector('.cnc-13__cta').addEventListener('click', (e) => {
e.currentTarget.textContent = 'You are on the list ✓';
say.textContent = 'You will be notified at launch';
});
addEventListener('pagehide', () => clearInterval(id), { once: true });
});
})();
```How this works
The one rule of countdowns: always recompute from the target date.
const target = new Date('2026-11-18T15:00:00Z').getTime();
const tick = () => {
const ms = Math.max(0, target - Date.now());
const d = Math.floor(ms / 864e5);
const h = Math.floor(ms / 36e5) % 24;
const m = Math.floor(ms / 6e4) % 60;
const s = Math.floor(ms / 1e3) % 60;
};A secs-- loop loses time whenever the tab is backgrounded, the device sleeps, or a frame is dropped; recomputing from Date.now() is always correct and self-heals after any pause. The interval is only a repaint trigger.
unit.style.setProperty('--cnc-13-p', s / 60); /* ring fraction */
.ring { background: conic-gradient(var(--acc) calc(var(--cnc-13-p) * 360deg), transparent 0) }Each unit carries its own progress ring — seconds out of 60, minutes out of 60, hours out of 24 — which turns four numbers into a readable, glanceable dial cluster. The digits use tabular-nums and a subtle translateY tick on change, and the live region announces at 24 h, 1 h, 10 min and zero rather than every second.
Make it yours
- Timezone honesty: render the target in the visitor's locale with
Intl.DateTimeFormat(undefined,{dateStyle:'full',timeStyle:'short'})underneath — global audiences need it. - Evergreen timers: for per-visitor deadlines, persist the target in localStorage on first visit instead of a fixed date.
- Compact bar version: hide the rings and lay the units in a row for a sticky announcement bar.
- Zero-state: swap the panel to a CTA when it hits zero — the code already flips a data-state attribute you can style freely.
Gotchas — read before shipping
- Never decrement a counter variable — it drifts. Recompute the delta from the target timestamp on every tick.
- setInterval(…, 1000) fires late under throttling; the visible seconds can skip. Recomputing means a skip shows the right number rather than a wrong one.
- Announcing every second with aria-live makes the page unusable with a screen reader. Announce at thresholds, and give the timer an accessible summary label.
- A countdown to a date without a timezone ('Nov 18, 3pm') is ambiguous. Use an ISO string with Z or an explicit offset.
- Hard-coded past dates make the demo show zeros forever — this build rolls the target forward if it has already elapsed.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
Date, setInterval and conic-gradient (Chrome 69 / Safari 12.1 / Firefox 83) are broadly supported. oklch/color-mix is the only modern floor and degrades to the fallback colours.