Aggiornamento: Timer e Countdown a una Data con JavaScript (2026)
Perché i Timer Countdown JavaScript Sono Ancora Fondamentali nel 2026
I countdown a una data specifica sono tra i componenti più richiesti nel frontend: lanci di prodotto, vendite flash, eventi live, scadenze di iscrizione. Nonostante sembrino semplici, costruire un timer robusto nasconde molte insidie—gestione del fuso orario, precisione al millisecondo, accessibilità, performance con setInterval che si desincronizza nel tempo.
Questo articolo è un refresh completo della guida originale, con tre novità importanti per il 2026: l’uso della Temporal API (ora in Stage 3 e supportata nativamente in Firefox e con polyfill stabile su Chrome/Safari), l’ottimizzazione tramite Web Workers per non bloccare il main thread, e la gestione corretta dei fusi orari IANA senza librerie esterne. Se usi ancora new Date() grezzo e un setInterval nudo, questa guida ti mostrerà perché nel 2026 si può fare molto meglio.
Troverai esempi di codice copiabili, dalla versione minimalista a quella production-ready con Worker, più una sezione dedicata all’integrazione con framework moderni come React e la gestione degli edge case più comuni. Se vuoi approfondire le animazioni collegate (es. il conto alla rovescia animato), dai un’occhiata alla guida sulle animazioni CSS moderne.
La Base: setInterval e Date.now()
Il modo classico per costruire un countdown è calcolare la differenza tra la data target e Date.now() ogni secondo tramite setInterval. È semplice, funziona ovunque, ma ha una debolezza: il browser può rallentare o mettere in pausa i timer quando la tab è in background (throttling). Ecco la versione base corretta:
/**
* Countdown base — versione classica 2026
* Funziona in tutti i browser moderni senza dipendenze
*/
function startCountdown(targetDate, onTick, onExpire) {
const target = new Date(targetDate).getTime();
function tick() {
const now = Date.now();
const diff = target - now;
if (diff <= 0) {
onExpire();
return; // non richiamo setInterval
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
onTick({ days, hours, minutes, seconds, diff });
}
tick(); // chiama subito senza aspettare 1 secondo
const id = setInterval(tick, 1000);
return () => clearInterval(id); // restituisce cleanup function
}
// Uso
const stop = startCountdown(
"2026-12-31T23:59:59",
({ days, hours, minutes, seconds }) => {
document.getElementById("cd-days").textContent = String(days).padStart(2, "0");
document.getElementById("cd-hours").textContent = String(hours).padStart(2, "0");
document.getElementById("cd-minutes").textContent = String(minutes).padStart(2, "0");
document.getElementById("cd-seconds").textContent = String(seconds).padStart(2, "0");
},
() => { document.getElementById("countdown").textContent = "Scaduto!"; }
);
// Per fermare: stop()
Nota come la funzione restituisce un cleanup: indispensabile per evitare memory leak nei componenti React o nelle SPA che montano e smontano componenti. Il tick() iniziale evita il secondo di ritardo visivo alla prima render.
Temporal API: il Modo Corretto per il 2026
La Temporal API risolve i problemi storici di Date: mutabilità, ambiguità sul fuso orario, calcoli che producono risultati sbagliati a cavallo di ore legali. Con Temporal puoi specificare esplicitamente la timezone IANA e ottenere una differenza precisa:
// Installa polyfill se necessario: npm install @js-temporal/polyfill
// In Node.js 22+ e Firefox 128+ Temporal è nativo
import { Temporal } from "@js-temporal/polyfill";
/**
* Countdown con Temporal API — timezone-safe
*/
function startTemporalCountdown(isoTarget, timeZone, onTick, onExpire) {
// Crea un ZonedDateTime nel fuso specificato
const target = Temporal.ZonedDateTime.from({
...Temporal.Instant.from(isoTarget).toZonedDateTimeISO(timeZone).toPlainDateTime(),
timeZone,
});
function tick() {
const now = Temporal.Now.zonedDateTimeISO(timeZone);
const diff = target.since(now, { largestUnit: "days" });
if (Temporal.ZonedDateTime.compare(target, now) <= 0) {
onExpire();
return;
}
onTick({
days: diff.days,
hours: diff.hours,
minutes: diff.minutes,
seconds: diff.seconds,
});
}
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}
// Uso: countdown per un evento a Roma il 31/12/2026
const stop = startTemporalCountdown(
"2026-12-31T23:59:59+01:00",
"Europe/Rome",
({ days, hours, minutes, seconds }) => {
console.log(`${days}g ${hours}h ${minutes}m ${seconds}s`);
},
() => console.log("Buon Anno!")
);
La differenza chiave: con Temporal.Now.zonedDateTimeISO("Europe/Rome") ottieni l’ora corrente già corretta per DST. Se il tuo evento è in estate (UTC+2) e calcoli in inverno (UTC+1), Date grezzo ti darà un’ora di scarto; Temporal no. Questo è particolarmente rilevante per siti che gestiscono eventi internazionali. Se costruisci app con TypeScript, abbina Temporal alla guida su TypeScript 5.x per tipizzare correttamente i parametri.
Web Workers: Countdown Off-Thread per Non Bloccare la UI
Il problema di setInterval nel main thread è che il browser può ritardarlo quando la CPU è occupata (layout, paint, script pesanti). Per countdown precisi al secondo in applicazioni complesse, la soluzione è spostare il timer in un Web Worker. Il Worker gira in un thread separato e non subisce il throttling del main thread.
// ── countdown.worker.js ──────────────────────────────────────────────────────
self.onmessage = function (e) {
const { action, targetTimestamp } = e.data;
if (action === "start") {
const id = setInterval(() => {
const diff = targetTimestamp - Date.now();
if (diff <= 0) {
clearInterval(id);
self.postMessage({ expired: true });
return;
}
self.postMessage({
expired: false,
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000),
});
}, 1000);
}
if (action === "stop") {
self.close();
}
};
// ── main.js ──────────────────────────────────────────────────────────────────
const worker = new Worker(new URL("./countdown.worker.js", import.meta.url));
worker.postMessage({
action: "start",
targetTimestamp: new Date("2026-12-31T23:59:59").getTime(),
});
worker.onmessage = (e) => {
if (e.data.expired) {
document.getElementById("countdown").textContent = "Scaduto!";
worker.terminate();
return;
}
const { days, hours, minutes, seconds } = e.data;
document.getElementById("cd-days").textContent = String(days).padStart(2, "0");
document.getElementById("cd-hours").textContent = String(hours).padStart(2, "0");
document.getElementById("cd-minutes").textContent = String(minutes).padStart(2, "0");
document.getElementById("cd-seconds").textContent = String(seconds).padStart(2, "0");
};
// Cleanup al destroy del componente
// worker.terminate();
Questa tecnica è particolarmente utile nelle SPA con molte animazioni CSS attive contemporaneamente. Per approfondire l’argomento Web Workers in contesti di performance avanzata, leggi l’articolo dedicato su Web Workers JavaScript e performance.
Integrazione React: Hook useCountdown Riutilizzabile
In React il pattern corretto prevede un custom hook che gestisce il ciclo di vita del timer—start al mount, cleanup all’unmount—e restituisce i valori già formattati:
// hooks/useCountdown.js
import { useState, useEffect, useRef } from "react";
/**
* Hook per countdown a una data target
* @param {string | number} targetDate - ISO string o timestamp
* @returns {{ days, hours, minutes, seconds, isExpired }}
*/
export function useCountdown(targetDate) {
const targetMs = useRef(new Date(targetDate).getTime());
const calculate = () => {
const diff = targetMs.current - Date.now();
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: true };
return {
days: Math.floor(diff / 86400000),
hours: Math.floor((diff % 86400000) / 3600000),
minutes: Math.floor((diff % 3600000) / 60000),
seconds: Math.floor((diff % 60000) / 1000),
isExpired: false,
};
};
const [state, setState] = useState(calculate);
useEffect(() => {
if (state.isExpired) return;
const id = setInterval(() => setState(calculate), 1000);
return () => clearInterval(id); // cleanup automatico
}, [state.isExpired]);
return state;
}
// ── Uso nel componente ────────────────────────────────────────────────────────
import { useCountdown } from "./hooks/useCountdown";
export function ProductLaunchBanner() {
const { days, hours, minutes, seconds, isExpired } = useCountdown("2026-11-01T10:00:00");
if (isExpired) return <p>Il lancio è avvenuto!</p>;
return (
<div className="countdown-banner">
<span>{String(days).padStart(2, "0")}g</span>
<span>{String(hours).padStart(2, "0")}h</span>
<span>{String(minutes).padStart(2, "0")}m</span>
<span>{String(seconds).padStart(2, "0")}s</span>
</div>
);
}
Il useRef per targetMs evita che il valore venga ricreato ad ogni render. La dipendenza [state.isExpired] nell’effect ferma automaticamente il timer quando scade. Per pattern avanzati di stato in React, vedi la guida a React 19 e Server Components.
HTML, CSS e Accessibilità del Countdown
Un countdown visivamente bello ma non accessibile è un problema: gli screen reader leggeranno ogni secondo i valori aggiornati, creando un rumore continuo. La soluzione è usare aria-live="off" sul container e aggiornare periodicamente un elemento aria-live="polite" separato (es. ogni minuto):
<!-- Struttura HTML accessibile per countdown -->
<section
class="countdown"
role="timer"
aria-label="Conto alla rovescia al lancio del prodotto"
>
<!-- Valori visivi: aria-hidden, non letti dallo screen reader ogni secondo -->
<div class="cd-unit" aria-hidden="true">
<span id="cd-days" class="cd-value">00</span>
<span class="cd-label">Giorni</span>
</div>
<div class="cd-unit" aria-hidden="true">
<span id="cd-hours" class="cd-value">00</span>
<span class="cd-label">Ore</span>
</div>
<div class="cd-unit" aria-hidden="true">
<span id="cd-minutes" class="cd-value">00</span>
<span class="cd-label">Minuti</span>
</div>
<div class="cd-unit" aria-hidden="true">
<span id="cd-seconds" class="cd-value">00</span>
<span class="cd-label">Secondi</span>
</div>
<!-- Aggiornato ogni minuto per screen reader: non disturba -->
<p id="cd-sr-status" aria-live="polite" class="sr-only"></p>
</section>
<!-- CSS minimo -->
<style>
.countdown { display: flex; gap: 1rem; justify-content: center; }
.cd-unit { text-align: center; }
.cd-value { display: block; font-size: clamp(2rem, 6vw, 5rem); font-weight: 800; font-variant-numeric: tabular-nums; }
.cd-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.6; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
</style>
Nota font-variant-numeric: tabular-nums: garantisce che le cifre occupino sempre la stessa larghezza, evitando il fastidioso balzellamento del layout quando i secondi passano da 10 a 9. È un dettaglio piccolo ma fa la differenza in un’interfaccia professionale.
🔧 Pro tip: Usa
font-variant-numeric: tabular-numssul wrapper del countdown per evitare il layout shift quando i numeri cambiano da 2 a 1 cifra.
Edge Case: Tab in Background, Ora Legale e Fusi Orari
Tre problemi reali che i tutorial classici ignorano:
- Tab throttling: Chrome/Firefox rallentano i timer nelle tab in background a max 1 Hz (o addirittura a 1 tick/minuto per battery saving). Se l’utente torna sulla tab, il countdown sembrerà “saltare”. Soluzione: al tick, ricalcola sempre da
Date.now()(come negli esempi sopra), mai decrementare un contatore interno. - Ora legale (DST): se la data target cade in un giorno in cui scatta l’ora legale,
new Date()gestisce il cambio correttamente per il fuso locale, ma se specifichi una data con offset fisso (+01:00invece diEurope/Rome) potresti ottenere un’ora di errore. Usa Temporal con IANA timezone oppure verifica che l’ISO string includa l’offset corretto. - Orologio di sistema manipolato: alcuni utenti cambiano l’orologio. Per countdown critici (es. offerta flash) verifica il tempo lato server e usa il delta dalla risposta iniziale come base.
💡 Insight: Calcola sempre
target - Date.now()ad ogni tick invece di decrementare un contatore: è l’unico modo per essere robusti al throttling delle tab in background.
FAQ e Domande Frequenti
Qual è la differenza tra setInterval e requestAnimationFrame per un countdown?
setInterval(fn, 1000) è la scelta corretta per i countdown: si attiva ogni secondo indipendentemente dalla frequenza di aggiornamento dello schermo. requestAnimationFrame si attiva a ogni frame (60fps), rendendo inutilmente costoso un aggiornamento che avviene solo ogni secondo. Usa rAF solo se vuoi animare un barra di progresso che si svuota in modo fluido al millisecondo, non per i digit del countdown.
La Temporal API è già pronta per la produzione nel 2026?
Nativa in Firefox 128+ e Node.js 22+; per Chrome e Safari serve ancora il polyfill @js-temporal/polyfill (npm). Il polyfill è stabile e il bundle aggiunto è circa 45 KB minificati. Per progetti nuovi: sì, usala pure con polyfill. Per aggiornare codice esistente: valuta il rapporto costo/beneficio. Se il tuo unico uso è un countdown semplice, il classico Date è ancora accettabile.
Come mostrare il countdown in un fuso orario diverso da quello dell’utente?
Con la Temporal API: passa la timezone IANA dell’evento (es. "America/New_York") al parametro timeZone della funzione mostrata sopra. Il calcolo si baserà su quell’ora, non sull’ora locale del browser. Con il vecchio Date: converti il target in UTC timestamp e usalo come numero fisso, poi ricalcola la diff da Date.now() (che è già UTC).
Come gestire il caso in cui il countdown sia già scaduto al caricamento della pagina?
In tutti gli esempi sopra, la funzione chiama tick() immediatamente prima di avviare setInterval. Se diff <= 0 al primo tick, il callback onExpire viene chiamato subito e il setInterval non parte. Ricordati di mostrare un messaggio alternativo nell’UI (“Scaduto”, “L’evento è terminato”) e di non lasciare il DOM con “00:00:00” fisso senza spiegazione.
Conclusione
Costruire un timer countdown in JavaScript nel 2026 non è più solo setInterval + new Date(): hai a disposizione la Temporal API per eliminare i bug di timezone, i Web Workers per non bloccare il main thread nelle app complesse, e pattern React testati per gestire il ciclo di vita correttamente. La versione base con Date.now() va ancora bene per la maggior parte dei casi; scala verso Temporal o Worker quando la precisione e la robustezza diventano requisiti reali.
Ricorda sempre: calcola la differenza da Date.now() ad ogni tick, usa tabular-nums per evitare layout shift, e gestisci l’accessibilità con aria-live="polite" su un elemento separato. Questi tre dettagli trasformano un countdown funzionante in un countdown professionale. Se il tuo progetto prevede anche animazioni avanzate attorno al countdown, l’articolo su animazioni JavaScript per UI e UX è il passo successivo.
Suggerimenti e Risorse
🔧 Strumento: Installa il polyfill Temporal con
npm install @js-temporal/polyfille importalo solo dove serve. Puoi usare code splitting per non aumentare il bundle globale.
💡 Pro tip: Per i countdown su pagine di vendita flash, sincronizza il timestamp di partenza con una risposta API del tuo server: protegge da utenti che modificano l’orologio di sistema per “fregare” le offerte.
🎯 Best practice: Testa sempre il tuo countdown impostando l’orario di sistema del browser 10 minuti prima della data target: verifica che scada correttamente, che il messaggio di “scaduto” appaia e che nessun
setIntervalrimanga attivo in background (controllalo con il pannello Performance di Chrome DevTools).

