NNicheStackTutorials
← All tutorials
Frontend Tools

launchtimer: Embeddable Countdown for Product Hunt Launches

SvelteKit static export needed a launch countdown with timezone display and localStorage dismiss — 95 lines, embed via iframe or snippet.

·10 min read·Frontend Tools
launchtimer: Embeddable Countdown for Product Hunt Launches cover

Launch: Tuesday 9am PT. Marketing wanted countdown on Hugo landing page without React hydration.

Full widget

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Launch Countdown</title>
  <style>
    body { font-family: system-ui; margin: 0; padding: 1.5rem; text-align: center; background: #0f172a; color: #f8fafc; }
    .grid { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
    .unit { background: #1e293b; padding: 1rem 1.25rem; border-radius: 12px; min-width: 70px; }
    .unit strong { display: block; font-size: 2rem; }
    .unit span { font-size: 12px; opacity: .7; text-transform: uppercase; }
    .done { font-size: 1.5rem; color: #4ade80; }
  </style>
</head>
<body>
  <h1 id="title">Launching in</h1>
  <div class="grid" id="grid">
    <div class="unit"><strong id="d">0</strong><span>days</span></div>
    <div class="unit"><strong id="h">0</strong><span>hours</span></div>
    <div class="unit"><strong id="m">0</strong><span>mins</span></div>
    <div class="unit"><strong id="s">0</strong><span>secs</span></div>
  </div>
  <p id="tz"></p>
  <script>
    const TARGET = new Date('2026-06-15T16:00:00Z');
    const KEY = 'launch-dismissed';

    if (localStorage.getItem(KEY)) {
      document.body.innerHTML = '<p class="done">We\'re live — thanks for waiting!</p>';
    } else {
      document.getElementById('tz').textContent = 'Target: ' + TARGET.toLocaleString();

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

      function tick() {
        const now = Date.now();
        let diff = TARGET - now;
        if (diff <= 0) {
          document.getElementById('grid').innerHTML = '<p class="done">🚀 We\'re live!</p>';
          localStorage.setItem(KEY, '1');
          return;
        }
        const sec = Math.floor(diff / 1000);
        document.getElementById('d').textContent = Math.floor(sec / 86400);
        document.getElementById('h').textContent = pad(Math.floor((sec % 86400) / 3600));
        document.getElementById('m').textContent = pad(Math.floor((sec % 3600) / 60));
        document.getElementById('s').textContent = pad(sec % 60);
      }

      tick();
      setInterval(tick, 1000);
    }
  </script>
</body>
</html>

Safari Date parse

Use ISO 2026-06-15T16:00:00Z — avoid 06/15/2026 which parses as local inconsistently.

Embed on static site

<iframe src="/tools/countdown.html" width="100%" height="200" style="border:0"></iframe>

Tab throttle

Browsers throttle setInterval in background tabs. On return, tick() runs once — drift max 1s, acceptable.

Verify

Set TARGET 2 min ahead → counts down After zero → shows live message Reload after dismiss → stays on live message

Tags: CountdownWidgetVanilla JSMarketing