Open demo

CSS code

HTML
<main class="cd">
  <a class="cd-logo" href="#"><svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="8" fill="#4338ca"/><path d="M7 16c2.5-3 4.5-3 7 0s4.5 3 7 0M7 11c2.5-3 4.5-3 7 0s4.5 3 7 0" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/></svg>Tidepool</a>
  <section class="cd-main" aria-labelledby="cd-title">
    <p class="cd-chip"><b>Beta</b> Invite list open for early teams</p>
    <h1 class="cd-title" id="cd-title">The shared inbox for small support teams opens soon.</h1>
    <p class="cd-lead">Tidepool puts every customer email, chat and form reply in one queue, with the context your team needs to answer it once.</p>
    <div class="cd-timer" data-countdown role="timer" aria-label="Time until launch">
      <div class="cd-unit"><span class="cd-num" data-cd="days">29</span><span class="cd-lbl">Days</span></div>
      <div class="cd-unit"><span class="cd-num" data-cd="hours">14</span><span class="cd-lbl">Hours</span></div>
      <div class="cd-unit"><span class="cd-num" data-cd="minutes">36</span><span class="cd-lbl">Minutes</span></div>
      <div class="cd-unit"><span class="cd-num" data-cd="seconds">12</span><span class="cd-lbl">Seconds</span></div>
    </div>
    <p class="cd-when">Doors open <time data-cd-date>on launch day at 09:00</time></p>
    <p class="sr-only" data-cd-say aria-live="polite"></p>
    <form class="cd-form" data-signup="cd-done" action="#" method="post">
      <label class="sr-only" for="cd-email">Work email</label>
      <div class="cd-field">
        <input id="cd-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="cd-err cd-note">
        <button type="submit">Get an invite</button>
      </div>
      <p class="cd-err" id="cd-err" data-error aria-live="polite"></p>
      <p class="cd-note" id="cd-note">Free for teams of up to five during the beta.</p>
    </form>
    <div class="cd-done" id="cd-done" tabindex="-1" hidden><p><strong>Your invite is reserved.</strong> It will arrive on launch morning, before the doors open to everyone else.</p></div>
  </section>
  <p class="cd-foot">Questions? <a href="mailto:[email protected]">[email protected]</a></p>
</main>
CSS
/* Countdown launch page: four digit tiles, a readable launch date, and an invite form. */
.cd{--cd-ink:#0f172a;--cd-muted:#475569;--cd-accent:#4338ca;--cd-accent-hover:#3730a3;--cd-line:#8391a7;--cd-err:#b91c1c;
  line-height:1.5;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;align-items:center;padding:28px 20px 36px;text-align:center;
  color:var(--cd-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:radial-gradient(60% 55% at 15% 0%,#dbe4ff 0,transparent 70%),radial-gradient(55% 60% at 100% 100%,#ede4ff 0,transparent 70%),#f8fafc}
.cd [hidden]{display:none!important}
.cd a{color:inherit}
.cd a:focus-visible,.cd button:focus-visible,.cd input:focus-visible{outline:2px solid var(--cd-accent);outline-offset:3px}
.cd-logo{display:inline-flex;align-items:center;gap:10px;font-weight:800;font-size:1.1rem;letter-spacing:-.02em;text-decoration:none}
.cd-main{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;max-width:760px;padding:48px 0}
.cd-chip{display:inline-flex;align-items:center;gap:8px;margin:0 0 22px;padding:6px 14px 6px 8px;font-size:.82rem;font-weight:600;
  color:#3730a3;background:#fff;border:1px solid #d9dcf5;border-radius:999px;box-shadow:0 4px 14px -8px rgba(67,56,202,.4)}
.cd-chip b{padding:2px 8px;font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;color:#fff;background:var(--cd-accent);border-radius:999px}
.cd-title{margin:0 0 16px;font-size:clamp(2rem,5.4vw,3.4rem);line-height:1.08;letter-spacing:-.035em;font-weight:800}
.cd-lead{max-width:540px;margin:0 0 36px;font-size:1.06rem;line-height:1.6;color:var(--cd-muted)}
.cd-timer{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(8px,2vw,16px);width:100%;max-width:560px;margin:0 0 16px}
.cd-unit{padding:clamp(14px,3vw,22px) 6px;background:#fff;border:1px solid #e2e8f0;border-radius:18px;
  box-shadow:0 1px 2px rgba(15,23,42,.05),0 14px 30px -18px rgba(67,56,202,.45)}
.cd-num{display:block;font-size:clamp(1.9rem,6vw,3.1rem);font-weight:800;line-height:1;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.cd-lbl{display:block;margin-top:8px;font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--cd-muted)}
.cd-when{margin:0 0 34px;font-size:.95rem;color:var(--cd-muted)}
.cd-when time{font-weight:700;color:var(--cd-ink)}
.cd-form{width:100%;max-width:480px}
.cd-field{display:flex;gap:8px;padding:6px;background:#fff;border:1px solid var(--cd-line);border-radius:14px}
.cd-field:has(input[aria-invalid="true"]){border-color:var(--cd-err);box-shadow:0 0 0 1px var(--cd-err)}
.cd-field input{flex:1;min-width:0;height:44px;padding:0 12px;font:inherit;color:var(--cd-ink);background:transparent;border:0;border-radius:9px}
.cd-field input::placeholder{color:#64748b}
.cd-field input:focus-visible{outline-offset:0}
.cd-field button{flex:none;height:44px;padding:0 20px;font:inherit;font-weight:700;color:#fff;background:var(--cd-accent);border:0;border-radius:9px;cursor:pointer;transition:background-color .2s}
.cd-field button:hover{background:var(--cd-accent-hover)}
.cd-err{min-height:1.4em;margin:10px 0 0;font-size:.88rem;color:var(--cd-err)}
.cd-note{margin:2px 0 0;font-size:.85rem;color:var(--cd-muted)}
.cd-done{width:100%;max-width:480px;padding:16px 20px;background:#fff;border:1px solid #c7d2fe;border-radius:14px;line-height:1.5}
.cd-done:focus{outline:none}
.cd-done p{margin:0}
.cd-foot{margin:0;font-size:.85rem;color:var(--cd-muted)}
.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:480px){.cd-unit{border-radius:14px}}
@media (max-width:575.98px){.cd-field{flex-direction:column}.cd-field input{flex:none}.cd-field button{width:100%}}
@media (prefers-reduced-motion:reduce){.cd *{transition:none}}
JavaScript
// Countdown. The demo launch is 09:00 local time, 30 days after the page loads, so it never reads zero.
// For a real launch, use a fixed moment instead: var launch = new Date('2026-11-05T09:00:00Z');
(function () {
  var timer = document.querySelector('[data-countdown]');
  if (!timer) return;
  var launch = new Date(); launch.setDate(launch.getDate() + 30); launch.setHours(9, 0, 0, 0);
  var cells = {}; timer.querySelectorAll('[data-cd]').forEach(function (el) { cells[el.getAttribute('data-cd')] = el; });
  var date = document.querySelector('[data-cd-date]'), say = document.querySelector('[data-cd-say]'), lastHour = null;
  if (date) {
    date.setAttribute('datetime', launch.toISOString());
    date.textContent = launch.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long' }) + ' at ' +
      launch.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
  }
  function pad(n) { return String(n).padStart(2, '0'); }
  function tick() {
    var left = Math.max(0, Math.floor((launch - Date.now()) / 1000));
    var d = Math.floor(left / 86400), h = Math.floor(left % 86400 / 3600), m = Math.floor(left % 3600 / 60);
    cells.days.textContent = d; cells.hours.textContent = pad(h); cells.minutes.textContent = pad(m); cells.seconds.textContent = pad(left % 60);
    // role="timer" is silent; this polite summary changes once an hour, so screen readers are not interrupted every second.
    if (say && h !== lastHour) { lastHour = h; say.textContent = d + ' days and ' + h + ' hours until launch.'; }
    if (!left) clearInterval(loop);
  }
  var loop = setInterval(tick, 1000); tick();
})();

// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<main class="cd d-flex flex-column align-items-center text-center">
  <a class="cd-logo d-inline-flex align-items-center gap-2 fw-bolder" href="#"><svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="8" fill="#4338ca"/><path d="M7 16c2.5-3 4.5-3 7 0s4.5 3 7 0M7 11c2.5-3 4.5-3 7 0s4.5 3 7 0" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/></svg>Tidepool</a>
  <section class="cd-main flex-grow-1 d-flex flex-column align-items-center justify-content-center w-100 py-5" aria-labelledby="cd-title">
    <p class="cd-chip badge rounded-pill d-inline-flex align-items-center gap-2 mb-4"><b class="rounded-pill text-uppercase text-white">Beta</b> Invite list open for early teams</p>
    <h1 class="cd-title fw-bolder mb-3" id="cd-title">The shared inbox for small support teams opens soon.</h1>
    <p class="cd-lead text-body-secondary mb-5">Tidepool puts every customer email, chat and form reply in one queue, with the context your team needs to answer it once.</p>
    <div class="cd-timer-wrap w-100 mb-3">
    <div class="cd-timer row row-cols-4" data-countdown role="timer" aria-label="Time until launch">
      <div class="col"><div class="card h-100"><span class="cd-num d-block fw-bolder" data-cd="days">29</span><span class="cd-lbl d-block mt-2 fw-bold text-uppercase text-body-secondary">Days</span></div></div>
      <div class="col"><div class="card h-100"><span class="cd-num d-block fw-bolder" data-cd="hours">14</span><span class="cd-lbl d-block mt-2 fw-bold text-uppercase text-body-secondary">Hours</span></div></div>
      <div class="col"><div class="card h-100"><span class="cd-num d-block fw-bolder" data-cd="minutes">36</span><span class="cd-lbl d-block mt-2 fw-bold text-uppercase text-body-secondary">Minutes</span></div></div>
      <div class="col"><div class="card h-100"><span class="cd-num d-block fw-bolder" data-cd="seconds">12</span><span class="cd-lbl d-block mt-2 fw-bold text-uppercase text-body-secondary">Seconds</span></div></div>
    </div>
    </div>
    <p class="text-body-secondary mb-4 pb-2">Doors open <time class="fw-bold text-body" data-cd-date>on launch day at 09:00</time></p>
    <p class="visually-hidden" data-cd-say aria-live="polite"></p>
    <form class="cd-form w-100" data-signup="cd-done" action="#" method="post">
      <label class="visually-hidden" for="cd-email">Work email</label>
      <div class="cd-field d-flex flex-column flex-sm-row gap-2">
        <input class="form-control" id="cd-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="cd-err cd-note">
        <button class="btn btn-cd flex-shrink-0" type="submit">Get an invite</button>
      </div>
      <p class="cd-err small mt-2 mb-0" id="cd-err" data-error aria-live="polite"></p>
      <p class="small text-body-secondary mb-0" id="cd-note">Free for teams of up to five during the beta.</p>
    </form>
    <div class="cd-done card w-100 px-4 py-3" id="cd-done" tabindex="-1" hidden><p class="m-0"><strong>Your invite is reserved.</strong> It will arrive on launch morning, before the doors open to everyone else.</p></div>
  </section>
  <p class="small text-body-secondary m-0">Questions? <a href="mailto:[email protected]">[email protected]</a></p>
</main>
CSS
/* Bootstrap grid and .card tiles for the countdown, .form-control and .btn for the invite form. */
.cd{--cd-accent:#4338ca;--cd-accent-hover:#3730a3;--cd-line:#8391a7;--cd-err:#b91c1c;
  --bs-body-color:#0f172a;--bs-secondary-color:#475569;--bs-border-color:#e2e8f0;--bs-card-border-radius:18px;
  --bs-body-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  min-height:100dvh;padding:28px 20px 36px;color:var(--bs-body-color);font-family:var(--bs-body-font-family);
  background:radial-gradient(60% 55% at 15% 0%,#dbe4ff 0,transparent 70%),radial-gradient(55% 60% at 100% 100%,#ede4ff 0,transparent 70%),#f8fafc}
.cd a{color:inherit}
.cd a:focus-visible{outline:2px solid var(--cd-accent);outline-offset:3px;border-radius:2px}
.cd-logo{font-size:1.1rem;letter-spacing:-.02em;text-decoration:none}
.cd-main{max-width:760px}
.cd-chip{--bs-badge-font-size:.82rem;--bs-badge-font-weight:600;--bs-badge-color:#3730a3;--bs-badge-padding-x:14px;--bs-badge-padding-y:6px;
  padding-left:8px;background:#fff;border:1px solid #d9dcf5;box-shadow:0 4px 14px -8px rgba(67,56,202,.4)}
.cd-chip b{padding:2px 8px;font-size:.72rem;letter-spacing:.06em;background:var(--cd-accent)}
.cd-title{font-size:clamp(2rem,5.4vw,3.4rem);line-height:1.08;letter-spacing:-.035em}
.cd-lead{max-width:540px;font-size:1.06rem;line-height:1.6}
.cd-timer-wrap{max-width:560px}
.cd-timer{--bs-gutter-x:clamp(8px,2vw,16px)}
.cd-timer .card{padding:clamp(14px,3vw,22px) 6px;box-shadow:0 1px 2px rgba(15,23,42,.05),0 14px 30px -18px rgba(67,56,202,.45)}
.cd-num{font-size:clamp(1.9rem,6vw,3.1rem);line-height:1;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.cd-lbl{font-size:.68rem;letter-spacing:.08em}
.cd-form,.cd-done{max-width:480px}
.cd-field{padding:6px;background:#fff;border:1px solid var(--cd-line);border-radius:14px}
.cd-field:has(.form-control[aria-invalid="true"]){border-color:var(--cd-err);box-shadow:0 0 0 1px var(--cd-err)}
.cd-field .form-control{height:44px;padding:0 12px;border:0;border-radius:9px;background:transparent}
.cd-field .form-control::placeholder{color:#64748b}
.cd-field .form-control:focus{box-shadow:none;outline:2px solid var(--cd-accent);outline-offset:0}
.cd .btn-cd{--bs-btn-bg:var(--cd-accent);--bs-btn-border-color:var(--cd-accent);--bs-btn-color:#fff;--bs-btn-font-weight:700;
  --bs-btn-hover-bg:var(--cd-accent-hover);--bs-btn-hover-border-color:var(--cd-accent-hover);--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:var(--cd-accent-hover);--bs-btn-active-border-color:var(--cd-accent-hover);--bs-btn-active-color:#fff;
  --bs-btn-focus-box-shadow:0 0 0 2px #fff,0 0 0 4px var(--cd-accent);--bs-btn-padding-x:20px;--bs-btn-border-radius:9px;height:44px}
.cd-err{min-height:1.4em;color:var(--cd-err)}
.cd-done{border:1px solid #c7d2fe;border-radius:14px}
.cd-done:focus{outline:none}
@media (prefers-reduced-motion:reduce){.cd .btn,.cd .form-control{transition:none}}
JavaScript
// Countdown. The demo launch is 09:00 local time, 30 days after the page loads, so it never reads zero.
// For a real launch, use a fixed moment instead: var launch = new Date('2026-11-05T09:00:00Z');
(function () {
  var timer = document.querySelector('[data-countdown]');
  if (!timer) return;
  var launch = new Date(); launch.setDate(launch.getDate() + 30); launch.setHours(9, 0, 0, 0);
  var cells = {}; timer.querySelectorAll('[data-cd]').forEach(function (el) { cells[el.getAttribute('data-cd')] = el; });
  var date = document.querySelector('[data-cd-date]'), say = document.querySelector('[data-cd-say]'), lastHour = null;
  if (date) {
    date.setAttribute('datetime', launch.toISOString());
    date.textContent = launch.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long' }) + ' at ' +
      launch.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
  }
  function pad(n) { return String(n).padStart(2, '0'); }
  function tick() {
    var left = Math.max(0, Math.floor((launch - Date.now()) / 1000));
    var d = Math.floor(left / 86400), h = Math.floor(left % 86400 / 3600), m = Math.floor(left % 3600 / 60);
    cells.days.textContent = d; cells.hours.textContent = pad(h); cells.minutes.textContent = pad(m); cells.seconds.textContent = pad(left % 60);
    // role="timer" is silent; this polite summary changes once an hour, so screen readers are not interrupted every second.
    if (say && h !== lastHour) { lastHour = h; say.textContent = d + ' days and ' + h + ' hours until launch.'; }
    if (!left) clearInterval(loop);
  }
  var loop = setInterval(tick, 1000); tick();
})();

// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<main class="flex min-h-dvh flex-col items-center bg-[radial-gradient(60%_55%_at_15%_0%,#dbe4ff_0,transparent_70%),radial-gradient(55%_60%_at_100%_100%,#ede4ff_0,transparent_70%)] bg-[#f8fafc] px-5 pt-7 pb-9 text-center font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] text-cd-ink">
  <a class="inline-flex items-center gap-2.5 text-[1.1rem] font-extrabold tracking-[-.02em] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-cd-accent" href="#"><svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="8" fill="#4338ca"/><path d="M7 16c2.5-3 4.5-3 7 0s4.5 3 7 0M7 11c2.5-3 4.5-3 7 0s4.5 3 7 0" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/></svg>Tidepool</a>
  <section class="flex w-full max-w-[760px] flex-1 flex-col items-center justify-center py-12" aria-labelledby="cd-title">
    <p class="mb-[22px] inline-flex items-center gap-2 rounded-full border border-[#d9dcf5] bg-white py-1.5 pr-3.5 pl-2 text-[.82rem] font-semibold text-[#3730a3] shadow-[0_4px_14px_-8px_rgba(67,56,202,.4)]"><b class="rounded-full bg-cd-accent px-2 py-0.5 text-[.72rem] tracking-[.06em] text-white uppercase">Beta</b> Invite list open for early teams</p>
    <h1 class="mb-4 text-[clamp(2rem,5.4vw,3.4rem)] leading-[1.08] font-extrabold tracking-[-.035em]" id="cd-title">The shared inbox for small support teams opens soon.</h1>
    <p class="mb-9 max-w-[540px] text-[1.06rem] leading-[1.6] text-cd-muted">Tidepool puts every customer email, chat and form reply in one queue, with the context your team needs to answer it once.</p>
    <div class="mb-4 grid w-full max-w-[560px] grid-cols-4 gap-[clamp(8px,2vw,16px)]" data-countdown role="timer" aria-label="Time until launch">
      <div class="rounded-[14px] border border-slate-200 bg-white px-1.5 py-[clamp(14px,3vw,22px)] shadow-[0_1px_2px_rgba(15,23,42,.05),0_14px_30px_-18px_rgba(67,56,202,.45)] min-[481px]:rounded-[18px]"><span class="block text-[clamp(1.9rem,6vw,3.1rem)] leading-none font-extrabold tracking-[-.03em] tabular-nums" data-cd="days">29</span><span class="mt-2 block text-[.68rem] font-bold tracking-[.08em] text-cd-muted uppercase">Days</span></div>
      <div class="rounded-[14px] border border-slate-200 bg-white px-1.5 py-[clamp(14px,3vw,22px)] shadow-[0_1px_2px_rgba(15,23,42,.05),0_14px_30px_-18px_rgba(67,56,202,.45)] min-[481px]:rounded-[18px]"><span class="block text-[clamp(1.9rem,6vw,3.1rem)] leading-none font-extrabold tracking-[-.03em] tabular-nums" data-cd="hours">14</span><span class="mt-2 block text-[.68rem] font-bold tracking-[.08em] text-cd-muted uppercase">Hours</span></div>
      <div class="rounded-[14px] border border-slate-200 bg-white px-1.5 py-[clamp(14px,3vw,22px)] shadow-[0_1px_2px_rgba(15,23,42,.05),0_14px_30px_-18px_rgba(67,56,202,.45)] min-[481px]:rounded-[18px]"><span class="block text-[clamp(1.9rem,6vw,3.1rem)] leading-none font-extrabold tracking-[-.03em] tabular-nums" data-cd="minutes">36</span><span class="mt-2 block text-[.68rem] font-bold tracking-[.08em] text-cd-muted uppercase">Minutes</span></div>
      <div class="rounded-[14px] border border-slate-200 bg-white px-1.5 py-[clamp(14px,3vw,22px)] shadow-[0_1px_2px_rgba(15,23,42,.05),0_14px_30px_-18px_rgba(67,56,202,.45)] min-[481px]:rounded-[18px]"><span class="block text-[clamp(1.9rem,6vw,3.1rem)] leading-none font-extrabold tracking-[-.03em] tabular-nums" data-cd="seconds">12</span><span class="mt-2 block text-[.68rem] font-bold tracking-[.08em] text-cd-muted uppercase">Seconds</span></div>
    </div>
    <p class="mb-[34px] text-[.95rem] text-cd-muted">Doors open <time class="font-bold text-cd-ink" data-cd-date>on launch day at 09:00</time></p>
    <p class="sr-only" data-cd-say aria-live="polite"></p>
    <form class="w-full max-w-[480px]" data-signup="cd-done" action="#" method="post">
      <label class="sr-only" for="cd-email">Work email</label>
      <div class="flex flex-col gap-2 rounded-[14px] border border-cd-line bg-white p-1.5 has-aria-invalid:border-cd-err has-aria-invalid:shadow-[0_0_0_1px_var(--color-cd-err)] min-[576px]:flex-row">
        <input class="h-11 min-w-0 flex-none min-[576px]:flex-1 rounded-[9px] bg-transparent px-3 text-cd-ink placeholder:text-slate-500 focus-visible:outline-2 focus-visible:outline-cd-accent" id="cd-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="cd-err cd-note">
        <button class="h-11 flex-none cursor-pointer rounded-[9px] bg-cd-accent px-5 font-bold text-white transition-colors duration-200 hover:bg-cd-accent-hover focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-cd-accent motion-reduce:transition-none" type="submit">Get an invite</button>
      </div>
      <p class="mt-2.5 min-h-[1.4em] text-[.88rem] text-cd-err" id="cd-err" data-error aria-live="polite"></p>
      <p class="mt-0.5 text-[.85rem] text-cd-muted" id="cd-note">Free for teams of up to five during the beta.</p>
    </form>
    <div class="w-full max-w-[480px] rounded-[14px] border border-indigo-200 bg-white px-5 py-4 leading-normal focus:outline-none" id="cd-done" tabindex="-1" hidden><p><strong>Your invite is reserved.</strong> It will arrive on launch morning, before the doors open to everyone else.</p></div>
  </section>
  <p class="text-[.85rem] text-cd-muted">Questions? <a class="underline focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-cd-accent" href="mailto:[email protected]">[email protected]</a></p>
</main>
CSS
@theme {
  --color-cd-ink: #0f172a;
  --color-cd-muted: #475569;
  --color-cd-accent: #4338ca;
  --color-cd-accent-hover: #3730a3;
  --color-cd-line: #8391a7;
  --color-cd-err: #b91c1c;
}
JavaScript
// Countdown. The demo launch is 09:00 local time, 30 days after the page loads, so it never reads zero.
// For a real launch, use a fixed moment instead: var launch = new Date('2026-11-05T09:00:00Z');
(function () {
  var timer = document.querySelector('[data-countdown]');
  if (!timer) return;
  var launch = new Date(); launch.setDate(launch.getDate() + 30); launch.setHours(9, 0, 0, 0);
  var cells = {}; timer.querySelectorAll('[data-cd]').forEach(function (el) { cells[el.getAttribute('data-cd')] = el; });
  var date = document.querySelector('[data-cd-date]'), say = document.querySelector('[data-cd-say]'), lastHour = null;
  if (date) {
    date.setAttribute('datetime', launch.toISOString());
    date.textContent = launch.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long' }) + ' at ' +
      launch.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
  }
  function pad(n) { return String(n).padStart(2, '0'); }
  function tick() {
    var left = Math.max(0, Math.floor((launch - Date.now()) / 1000));
    var d = Math.floor(left / 86400), h = Math.floor(left % 86400 / 3600), m = Math.floor(left % 3600 / 60);
    cells.days.textContent = d; cells.hours.textContent = pad(h); cells.minutes.textContent = pad(m); cells.seconds.textContent = pad(left % 60);
    // role="timer" is silent; this polite summary changes once an hour, so screen readers are not interrupted every second.
    if (say && h !== lastHour) { lastHour = h; say.textContent = d + ' days and ' + h + ' hours until launch.'; }
    if (!left) clearInterval(loop);
  }
  var loop = setInterval(tick, 1000); tick();
})();

// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

About this set

A launch page built around a live countdown: four white tiles for days, hours, minutes and seconds on a soft indigo and lilac gradient, a Beta chip, a headline for a shared-inbox product, the launch date in words and an invite form in a single rounded field. Use it when there is a fixed launch moment worth counting down to, such as a product release, a beta opening or an event. The countdown is about twenty lines of vanilla JavaScript. In the demo the launch is set to 09:00 local time thirty days after the page loads, so it never shows zeros in a screenshot or a preview; a comment shows the one line to change for a real date. The script also writes the date as readable text into a time element with a datetime attribute. The tiles sit in a role=”timer” region, which screen readers do not announce, and a separate polite live region changes only once an hour with a sentence such as 29 days and 14 hours until launch. The invite form validates the address in the browser and shows a confirmation; nothing is sent. Four equal grid columns keep the tiles on one row down to 375 pixels.

What’s included

  • Days, hours, minutes and seconds in tabular figures
  • Demo date is always 30 days ahead, so the timer never reads zero
  • role="timer" plus a live region that speaks once an hour, not every second
  • Launch date written out in words with a machine-readable datetime
  • Invite form with inline validation and a confirmation state

Accessibility

The tiles are grouped in a role="timer" element named Time until launch, so the ticking seconds are not read out; a visually hidden polite live region updates once an hour instead. The email field has a hidden label, aria-describedby for its error and note, aria-invalid when wrong, and focus moves to the confirmation after a valid address. Links, the field and the button show a 2px indigo focus outline, and transitions are removed under prefers-reduced-motion.

Customise it

Set the real launch moment in the script (new Date('2026-11-05T09:00:00Z')). Colours are --cd-accent, --cd-ink and --cd-muted on .cd (plain CSS and Bootstrap) or the --color-cd-* theme colours in Tailwind; the tile size is the clamp() on .cd-num.