Updated July 2026 · MIT licensed · no account
Coming soon page in HTML: the full code, three files, no framework
Not a gallery of screenshots. The complete, copy-paste code for a working coming soon page — HTML, CSS and a real countdown in JavaScript — laid out in the order you build it. Every block has a one-click copy button, and the live demo below is the exact page the code produces. Then the same page written four ways: vanilla, Bootstrap 5, Tailwind and React, each labelled with its stack and its catch.
The one thing a tutorial owes you that most skip: the email field on a plain HTML file collects nothing until you wire it up. Step 4 is entirely about that, and there is an honest section on when to stop hand-coding and let the builder do it.
Live — this is what the code below renders
Coming soon
Something good is on the way
We open on 1 September. Leave your email and you'll be first in.
- 18Days
- 06Hours
- 42Min
- 00Sec
The countdown really is counting. Want the full-screen version? Open the Minimal demo.
Build it in four steps
Make one folder. Drop three files in it. Open index.html in a browser. That is the whole toolchain — no npm, no bundler, no server. Here is each file, in order.
The markup — index.html
One <main> holding four things: an eyebrow, a headline, the countdown list (four <span>s with ids the JS will find), and an email form. Note the comment on the form — action="#" is a placeholder that goes nowhere yet.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>We're launching soon</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="coming-soon">
<p class="eyebrow">Coming soon</p>
<h1>Something good is on the way</h1>
<p class="subtitle">We open on 1 September. Leave your email and you'll be first in.</p>
<!-- JS fills these four numbers every second -->
<ul class="countdown" id="countdown" aria-live="polite">
<li><span id="days">00</span><small>Days</small></li>
<li><span id="hours">00</span><small>Hours</small></li>
<li><span id="minutes">00</span><small>Minutes</small></li>
<li><span id="seconds">00</span><small>Seconds</small></li>
</ul>
<!-- action="#" collects NOTHING. Point it at a real endpoint — see step 4. -->
<form class="notify" action="#" method="post">
<input type="email" name="email" placeholder="[email protected]" required aria-label="Email address" />
<button type="submit">Notify me</button>
</form>
</main>
<script src="script.js"></script>
</body>
</html>Full-screen centring — style.css
min-height:100vh plus display:grid; place-items:center centres the whole thing horizontally and vertically with no positioning tricks. Colours live in three CSS variables at the top so you rebrand the page by editing one line. The last block stacks the form on phones.
:root {
--bg: #1a1b2e; /* navy, not pure black — pure black vibrates on OLED */
--accent: #7c5cfc; /* used exactly twice: countdown boxes and the button */
--text: #ffffff;
}
* { box-sizing: border-box; margin: 0; }
body {
min-height: 100vh;
display: grid;
place-items: center; /* dead-centre, no magic numbers */
padding: 24px;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--text);
background: radial-gradient(circle at 30% 20%, #2a2c52, var(--bg));
text-align: center;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: .2em;
font-size: .75rem;
color: var(--accent);
margin-bottom: 1rem;
}
h1 { font-size: clamp(2rem, 6vw, 3.5rem); line-height: 1.1; }
.subtitle { max-width: 32rem; margin: 1rem auto 2.5rem; opacity: .7; }
.countdown {
display: flex;
gap: 1rem;
justify-content: center;
list-style: none;
padding: 0;
margin-bottom: 2.5rem;
}
.countdown li {
background: rgba(124, 92, 252, .15);
border-radius: .75rem;
padding: 1rem;
min-width: 4.5rem;
}
.countdown span { display: block; font-size: 2rem; font-weight: 700; }
.countdown small { opacity: .5; font-size: .75rem; text-transform: uppercase; }
.notify { display: flex; gap: .5rem; max-width: 26rem; margin: 0 auto; width: 100%; }
.notify input {
flex: 1;
padding: .85rem 1rem;
border: 1px solid rgba(255, 255, 255, .2);
border-radius: .75rem;
background: rgba(255, 255, 255, .05);
color: var(--text);
}
.notify button {
padding: .85rem 1.5rem;
border: 0;
border-radius: .75rem;
background: var(--accent);
color: #fff;
font-weight: 600;
cursor: pointer;
}
/* Phones: stack the input and button, shrink the boxes */
@media (max-width: 480px) {
.countdown { gap: .5rem; }
.countdown li { min-width: 3.75rem; padding: .75rem; }
.notify { flex-direction: column; }
}The countdown — script.js
This is the snippet people come for. It reads a launch timestamp, works out the gap to now, and writes days/hours/minutes/seconds every second. Two details the copy-paste versions elsewhere drop: it pads single digits to 07, and it calls clearInterval at zero so the clock stops and swaps to “We're live!” instead of running negative.
// Your launch date. Months are 0-indexed in JS, so 8 = September.
// Prefer a plain string? new Date("2026-09-01T09:00:00").getTime()
const launch = new Date(2026, 8, 1, 9, 0, 0).getTime();
const els = {
days: document.getElementById("days"),
hours: document.getElementById("hours"),
minutes: document.getElementById("minutes"),
seconds: document.getElementById("seconds"),
};
const pad = (n) => String(n).padStart(2, "0"); // 7 -> "07"
// Start the clock FIRST so 'timer' exists even if the date is already past.
const timer = setInterval(tick, 1000);
tick(); // paint now, don't wait 1s
function tick() {
const diff = launch - Date.now();
// Reached launch: stop the interval and swap the whole block.
if (diff <= 0) {
clearInterval(timer);
document.getElementById("countdown").innerHTML = "<li>We're live!</li>";
return;
}
const sec = 1000, min = sec * 60, hr = min * 60, day = hr * 24;
els.days.textContent = pad(Math.floor(diff / day));
els.hours.textContent = pad(Math.floor((diff % day) / hr));
els.minutes.textContent = pad(Math.floor((diff % hr) / min));
els.seconds.textContent = pad(Math.floor((diff % min) / sec));
}Make the form actually collect emails
The step every other “coming soon HTML” tutorial leaves out. A static file has no inbox, so until you change action="#" the form drops every address in silence. Two honest fixes — a hosted form endpoint, or your own API with fetch():
<!-- A static file has no inbox. Until you change action="#" the form
posts nowhere and every signup is silently lost. Two ways to fix it: -->
<!-- Option A — a form endpoint (Formspree, Basin, Getform). Zero backend. -->
<form action="https://formspree.io/f/YOUR_ID" method="POST">
<input type="email" name="email" placeholder="[email protected]" required />
<button type="submit">Notify me</button>
</form>
<!-- Option B — your own endpoint, submitted with fetch() so the page doesn't reload -->
<script>
document.querySelector(".notify").addEventListener("submit", async (e) => {
e.preventDefault();
const email = e.target.email.value;
await fetch("/api/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
e.target.innerHTML = "<p>Thanks — we'll email once, at launch.</p>";
});
</script>Whichever you pick, submit the form yourself once and confirm the email landed before you post the link anywhere. A launch page that loses its first 200 signups looks identical to one that works — that is exactly why it bites people.
Same page, four stacks
The countdown logic never changes — only the markup and styling do. Pick the one your project already loads so you are not dragging in a framework for a single page. Every version is MIT licensed and responsive.
- Stack
- Bootstrap 5 utility classes, one CDN link, no build step
- Dependencies
- bootstrap 5.3 (CDN)
- Responsive
- Yes
- Licence
- MIT · free
Reach for this if the rest of your site already loads Bootstrap. You get the grid and spacing for free; the trade is ~30 KB of CSS you are not using here.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coming soon</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body class="bg-dark text-white">
<main class="d-flex flex-column justify-content-center align-items-center vh-100 text-center px-3">
<p class="text-uppercase small text-primary mb-2" style="letter-spacing:.2em">Coming soon</p>
<h1 class="display-4 fw-bold">Something good is on the way</h1>
<p class="text-white-50 mb-4">We open on 1 September. Leave your email to get in first.</p>
<ul class="list-unstyled d-flex gap-3 mb-4" id="countdown">
<li class="bg-primary bg-opacity-25 rounded-3 p-3" style="min-width:4.5rem">
<span class="fs-2 fw-bold d-block" id="days">00</span><small class="text-white-50">Days</small></li>
<li class="bg-primary bg-opacity-25 rounded-3 p-3" style="min-width:4.5rem">
<span class="fs-2 fw-bold d-block" id="hours">00</span><small class="text-white-50">Hours</small></li>
<li class="bg-primary bg-opacity-25 rounded-3 p-3" style="min-width:4.5rem">
<span class="fs-2 fw-bold d-block" id="minutes">00</span><small class="text-white-50">Minutes</small></li>
<li class="bg-primary bg-opacity-25 rounded-3 p-3" style="min-width:4.5rem">
<span class="fs-2 fw-bold d-block" id="seconds">00</span><small class="text-white-50">Seconds</small></li>
</ul>
<form class="d-flex gap-2" action="#" method="post" style="max-width:26rem;width:100%">
<input type="email" class="form-control" placeholder="[email protected]" required />
<button class="btn btn-primary px-4" type="submit">Notify me</button>
</form>
</main>
<script>
// Same logic as the vanilla JS step, just inlined.
const launch = new Date(2026, 8, 1, 9, 0, 0).getTime();
const pad = (n) => String(n).padStart(2, "0");
const el = (id) => document.getElementById(id);
const timer = setInterval(update, 1000);
update();
function update() {
const d = launch - Date.now();
if (d <= 0) { clearInterval(timer); el("countdown").innerHTML = "<li class='fs-3'>We're live!</li>"; return; }
el("days").textContent = pad(Math.floor(d / 864e5));
el("hours").textContent = pad(Math.floor(d % 864e5 / 36e5));
el("minutes").textContent = pad(Math.floor(d % 36e5 / 6e4));
el("seconds").textContent = pad(Math.floor(d % 6e4 / 1e3));
}
</script>
</body>
</html>Or start from a ready template
The four-step code above is one page, one palette. If you'd rather pick a look and change the words, here are all 14 templates on the site, side by side — every one exports the same three-file HTML/CSS/JS you just read, and every one has a live demo you can open and submit right now. 8 download as a free MIT .zip; the rest are hosted-only on the $9 plan.
| Template | Palette | Typeface | Countdown | Best for | Get it | Demo |
|---|---|---|---|---|---|---|
| Minimal | Dark | Inter | 21-day | Startup | Free · MIT .zip | Open → |
| Sunrise | Light | Poppins | 14-day | Product launch | Free · MIT .zip | Open → |
| Ocean | Dark | Inter | 30-day | App / SaaS waitlist | Free · MIT .zip | Open → |
| Aurora | Dark | Space Grotesk | 7-day | App / SaaS waitlist | Pro $9/mo | Open → |
| Forest | Dark | DM Sans | None | Startup | Pro $9/mo | Open → |
| Coral | Light | Poppins | 45-day | Shop / drop | Pro $9/mo | Open → |
| Paper | Light | Playfair Display | 60-day | Newsletter | Free · MIT .zip | Open → |
| Midnight | Dark | Space Grotesk | 10-day | Event | Free · MIT .zip | Open → |
| Mono | Light | DM Sans | None | Portfolio | Free · MIT .zip | Open → |
| Terracotta | Light | Montserrat | 35-day | Restaurant / café | Free · MIT .zip | Open → |
| Neon | Dark | Space Grotesk | 90-day | App / SaaS waitlist | Pro $9/mo | Open → |
| Blush | Light | Outfit | 21-day | Shop / drop | Pro $9/mo | Open → |
| Slate | Dark | Inter | None | Agency / freelance | Pro $9/mo | Open → |
| Lumen | Light | Outfit | 28-day | Startup | Free · MIT .zip | Open → |
One honest note the table hides: Forest, Mono, Slate ship without a countdown on purpose. A clock you end up extending twice reads worse than no date at all, so those three lead with a plain headline instead. You can still switch a countdown on for any of them in the customiser. Full write-ups — palette reasoning, contrast numbers, what to change first — live on the templates page.
Hand-code it, or skip to the finished page
The code above is genuinely all you need — take it, it is yours. But three of the four steps are the same every launch, and step 4 (a working inbox) is the one that eats an evening. Straight comparison:
</> Copy the code
- + Full control, no lock-in, MIT — edit anything
- + Free forever, no account, host anywhere
- – You wire the email form yourself (Formspree / API)
- – No leads dashboard or CSV — you build that too
- – Colour and copy changes mean editing files by hand
⚡ Customise & host it here
- + Same page, edited in-browser — headline, colours, date
- + Email field wired to a leads list with CSV export, no config
- + Live on a real URL in a click, or still export the .zip
- + Free plan: 3 pages, 100 leads, no card
- · Pro is $9/month for unlimited pages, custom domains, lead export
Both paths start from the same design. Prefer to see it applied to real launches first? The teardown of 11 real coming soon pages shows what companies like Dropbox and Robinhood actually shipped. On a specific stack? There’s a dedicated walkthrough for a coming soon page on WordPress (plugin vs the no-plugin theme method) and one for Shopify.
Questions developers ask
How do I make a coming soon page in HTML?
Three files. An index.html with a headline, a countdown list and an email form; a style.css that centres it full-screen with display:grid; place-items:center; and a script.js that updates the countdown every second. The full code for all three is on this page, ready to copy. Save them in one folder and open index.html — no build step, no framework.
How do I add a countdown timer to an HTML page?
Give each number an id (days, hours…), pick a launch timestamp with new Date(...).getTime(), then run a setInterval once a second that writes launch - Date.now() split into days/hours/minutes/seconds. The one thing most snippets forget: call clearInterval when the difference hits zero so it stops instead of counting into negatives. The commented JS above does exactly that.
Are these coming soon HTML templates free? What licence?
Every snippet on this page — vanilla, Bootstrap, Tailwind and React — is MIT licensed. Copy it, change it, ship it commercially, no attribution required and no account needed. The 8 downloadable .zip templates on the templates page carry the same MIT licence.
Why does my coming soon form not collect any emails?
Because a static HTML file has nowhere to send them. Ship it with action="#" and every address is silently dropped — no error, nothing in an inbox. You have to point the form at something: a service like Formspree, your own /api endpoint, or a host that wires the field for you. Step 4 above shows both DIY options. Whatever you pick, submit the form yourself once and confirm the email arrived before you share the link.
Can I use this with Bootstrap, Tailwind or React?
Yes — the same page is written four ways on this page. The countdown logic is identical across all of them; only the markup and styling change. Pick the one that matches what your project already loads so you are not pulling in a framework just for one page.
How do I host a coming soon HTML page?
Drag the folder onto Netlify Drop, push it to GitHub Pages, or upload the three files to any static host — it is plain HTML, so anywhere works and most are free. The catch is the email form: static hosting cannot receive submissions on its own, so you still need a form endpoint (step 4) or a host that handles it for you.
How do I change the launch date?
Edit one line: const launch = new Date(2026, 8, 1, 9, 0, 0). The arguments are year, month, day, hour, minute, second — and the month is 0-indexed, so 8 is September, not August. Set a date you will actually hit; if it slips, the timer will hit zero on an unchanged page in front of the exact people who signed up.
You have the code. Now make the form work.
The countdown you just copied, hosted on a real URL with an email field that keeps the addresses — no Formspree, no backend. Or export it as a standalone MIT .zip and wire it yourself. Your call.
Free plan: 3 pages, 100 leads, no card.