Date & Time

COUNTDOWN TIMER

Set a countdown to any future date. Live days, hours, minutes, seconds. Save multiple countdowns.

Quick presets
Saved Countdowns
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

let timer = null, startTs = null, targetTs = null;
  let prevValues = { days: null, hours: null, mins: null, secs: null };

  function pad(n) { return String(Math.floor(n)).padStart(2, '0'); }

  function toDatetimeLocalValue(ts) {
    const d = new Date(ts);
    return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
  }

  function setPreset(key) {
    const now = new Date();
    let target;
    if (key === 'newyear') target = new Date(now.getFullYear() + 1, 0, 1, 0, 0, 0);
    else if (key === '1week') target = new Date(now.getTime() + 7 * 86400000);
    else if (key === '1month') { target = new Date(now); target.setMonth(target.getMonth() + 1); }
    else if (key === '24h') target = new Date(now.getTime() + 86400000);
    else if (key === '1h') target = new Date(now.getTime() + 3600000);
    else if (key === '10m') target = new Date(now.getTime() + 600000);
    document.getElementById('targetDate').value = toDatetimeLocalValue(target.getTime());
    start();
  }

  function getUrgencyLevel(remaining) {
    if (remaining <= 0) return 'done';
    if (remaining < 60 * 60 * 1000) return 'crit';   // < 1 hour
    if (remaining < 24 * 60 * 60 * 1000) return 'warn'; // < 24 hours
    return 'normal';
  }

  function applyUrgency(level) {
    const display = document.getElementById('countdownDisplay');
    const bar = document.getElementById('progressBar');
    const badge = document.getElementById('urgencyBadge');
    const nums = document.querySelectorAll('.countdown-num');
    const pulse = document.getElementById('secPulse');

    display.classList.remove('urgency-warn', 'urgency-crit');
    bar.classList.remove('urgency-warn', 'urgency-crit');
    badge.classList.remove('show', 'warn', 'crit');
    nums.forEach(n => n.classList.remove('urgency-warn', 'urgency-crit'));
    pulse.classList.remove('urgency-crit');

    if (level === 'warn') {
      display.classList.add('urgency-warn');
      bar.classList.add('urgency-warn');
      nums.forEach(n => n.classList.add('urgency-warn'));
      badge.textContent = '< 24 hours remaining';
      badge.classList.add('show', 'warn');
    } else if (level === 'crit') {
      display.classList.add('urgency-crit');
      bar.classList.add('urgency-crit');
      nums.forEach(n => n.classList.add('urgency-crit'));
      pulse.classList.add('urgency-crit');
      badge.textContent = '< 1 hour remaining';
      badge.classList.add('show', 'crit');
    }
  }

  function flipNum(el, newVal) {
    if (el.textContent === newVal) return;
    el.classList.remove('flip-out', 'flip-in');
    el.classList.add('flip-out');
    setTimeout(() => {
      el.textContent = newVal;
      el.classList.remove('flip-out');
      el.classList.add('flip-in');
      setTimeout(() => el.classList.remove('flip-in'), 200);
    }, 120);
  }

  function start(fromHash = false) {
    const val = document.getElementById('targetDate').value;
    if (!val) {
      alert('Please select a target date and time.');
      return;
    }
    const ts = new Date(val).getTime();
    if (isNaN(ts)) {
      alert('Invalid date format.');
      return;
    }

    targetTs = ts;
    startTs = Date.now();
    prevValues = { days: null, hours: null, mins: null, secs: null };

    document.getElementById('setupArea').style.display = 'none';
    document.getElementById('activeArea').style.display = 'block';
    document.getElementById('countdownDisplay').style.display = '';
    document.getElementById('timesup').style.display = 'none';

    const ev = document.getElementById('eventName').value || 'COUNTDOWN';
    document.getElementById('displayEvent').textContent = ev.toUpperCase();
    document.getElementById('timesupEvent').textContent = ev.toUpperCase();
    document.getElementById('targetDateDisplay').textContent = 'Target: ' + new Date(targetTs).toLocaleString();

    // Only update hash when not triggered by hash change (prevents double-start)
    if (!fromHash) {
      const hashData = { t: targetTs, n: ev };
      const hashStr = encodeURIComponent(JSON.stringify(hashData));
      history.replaceState(null, '', '#' + hashStr);
    }

    if (timer) clearInterval(timer);
    tick();
    timer = setInterval(tick, 1000);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }

  function reset() {
    if (timer) clearInterval(timer);
    timer = null; targetTs = null;
    document.getElementById('setupArea').style.display = 'block';
    document.getElementById('activeArea').style.display = 'none';
    history.replaceState(null, '', location.pathname);
    applyUrgency('normal');
  }

  function tick() {
    const remaining = targetTs - Date.now();
    if (remaining <= 0) {
      clearInterval(timer);
      timer = null;
      document.getElementById('countdownDisplay').style.display = 'none';
      document.getElementById('timesup').style.display = '';
      document.getElementById('progressBar').style.width = '100%';
      launchConfetti();
      return;
    }

    const totalDuration = targetTs - startTs;
    const elapsed = Date.now() - startTs;
    const pct = Math.min(100, (elapsed / totalDuration) * 100);
    document.getElementById('progressBar').style.width = pct + '%';

    const secs = remaining / 1000;
    const days = Math.floor(secs / 86400);
    const hrs = Math.floor((secs % 86400) / 3600);
    const mins = Math.floor((secs % 3600) / 60);
    const s = Math.floor(secs % 60);

    const dStr = pad(days), hStr = pad(hrs), mStr = pad(mins), sStr = pad(s);

    flipNum(document.getElementById('cdDays'), dStr);
    flipNum(document.getElementById('cdHours'), hStr);
    flipNum(document.getElementById('cdMins'), mStr);
    flipNum(document.getElementById('cdSecs'), sStr);

    // Pulse dot on every second
    const pulse = document.getElementById('secPulse');
    pulse.classList.remove('beat');
    void pulse.offsetWidth; // reflow to restart animation
    pulse.classList.add('beat');

    applyUrgency(getUrgencyLevel(remaining));
  }

  // ── Confetti ──
  function launchConfetti() {
    const container = document.getElementById('confettiContainer');
    const colors = ['#ff2200', '#ff5555', '#00c896', '#f5c518', '#e8e0d5', '#ff8800'];
    container.innerHTML = '';
    for (let i = 0; i < 80; i++) {
      const el = document.createElement('div');
      el.className = 'confetti-piece';
      const color = colors[Math.floor(Math.random() * colors.length)];
      const left = Math.random() * 100;
      const dur = 2 + Math.random() * 2;
      const delay = Math.random() * 1.2;
      const spin = (Math.random() > 0.5 ? '' : '-') + (180 + Math.floor(Math.random() * 540)) + 'deg';
      const shape = Math.random() > 0.5 ? '50%' : '0';
      el.style.cssText = `left:${left}%;top:-20px;background:${color};border-radius:${shape};--dur:${dur}s;--delay:${delay}s;--spin:${spin};animation-delay:${delay}s;`;
      container.appendChild(el);
    }
    setTimeout(() => { container.innerHTML = ''; }, 5000);
  }

  // ── Save / Load / Share ──
  function saveCountdown() {
    const val = document.getElementById('targetDate').value;
    if (!val) return;
    const name = document.getElementById('eventName').value || 'Untitled';
    const saved = JSON.parse(localStorage.getItem('countdowns') || '[]');
    // Avoid duplicates by target timestamp
    const ts = new Date(val).getTime();
    const exists = saved.some(s => s.target === ts && s.name === name);
    if (!exists) {
      saved.unshift({ name, target: ts });
      localStorage.setItem('countdowns', JSON.stringify(saved.slice(0, 10)));
    }
    renderSaved();
  }

  function renderSaved() {
    const saved = JSON.parse(localStorage.getItem('countdowns') || '[]');
    const el = document.getElementById('savedList');
    if (!saved.length) { el.innerHTML = '<div style="font-size:12px;color:var(--muted);padding:8px 0;">No saved countdowns yet.</div>'; return; }
    el.innerHTML = saved.map((s, i) => `
      <div class="saved-item" onclick="loadSaved(${i})">
        <div>
          <div class="saved-name">${s.name.replace(/</g,'&lt;')}</div>
          <div class="saved-date">${new Date(s.target).toLocaleString()}</div>
        </div>
        <button class="saved-del" onclick="deleteSaved(event,${i})" title="Remove">×</button>
      </div>`).join('');
  }

  function loadSaved(i) {
    const saved = JSON.parse(localStorage.getItem('countdowns') || '[]');
    const s = saved[i];
    document.getElementById('eventName').value = s.name;
    document.getElementById('targetDate').value = toDatetimeLocalValue(s.target);
    start();
  }

  function deleteSaved(e, i) {
    e.stopPropagation();
    const saved = JSON.parse(localStorage.getItem('countdowns') || '[]');
    saved.splice(i, 1);
    localStorage.setItem('countdowns', JSON.stringify(saved));
    renderSaved();
  }

  function copyAndFlash(btnId) {
    navigator.clipboard.writeText(location.href).catch(() => {});
    const el = document.getElementById(btnId);
    if (!el) return;
    el.classList.add('show');
    setTimeout(() => el.classList.remove('show'), 2000);
  }

  function shareLink() { copyAndFlash('shareCopied2'); }

  function shareFromSetup() {
    const val = document.getElementById('targetDate').value;
    if (val) {
      const ev = document.getElementById('eventName').value || 'COUNTDOWN';
      const ts = new Date(val).getTime();
      if (!isNaN(ts)) {
        const hashStr = encodeURIComponent(JSON.stringify({ t: ts, n: ev }));
        history.replaceState(null, '', '#' + hashStr);
      }
    }
    copyAndFlash('shareCopied');
  }

  function loadFromHash() {
    try {
      const hash = decodeURIComponent(location.hash.slice(1));
      if (!hash) return;
      const data = JSON.parse(hash);
      if (data.t && data.n) {
        // Guard: if already running this exact countdown, do nothing
        if (targetTs === data.t) return;
        document.getElementById('eventName').value = data.n === 'COUNTDOWN' ? '' : data.n;
        document.getElementById('targetDate').value = toDatetimeLocalValue(data.t);
        start(true); // fromHash=true → won't re-set hash
      }
    } catch(e) {
      // malformed hash — ignore
    }
  }

  window.addEventListener('hashchange', loadFromHash);
  loadFromHash();
  renderSaved();