<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>
// 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>
// 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>
// 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.