EmpireUI
Get Pro
← Blog7 min read#tailwind-css#coming-soon-page#countdown-timer

Tailwind Coming Soon Page: Launch Countdown with Email Capture

Build a Tailwind CSS coming soon page with live countdown timer and email capture. Real code, no fluff — from layout to form handling in under 60 minutes.

Minimalist dark countdown timer display with glowing digits on a blurred background

Why Most Coming Soon Pages Are a Waste of Screen Space

Honestly, a coming soon page with just a logo and "we'll be back soon" is worse than having nothing at all. It collects no emails, communicates no value, and gives visitors zero reason to return. That's dead marketing traffic you'll never recover.

What you actually want is a page that works while you're still building. A countdown timer creates urgency. An email capture box turns passive visitors into warm leads. A short value prop tells people exactly why they should care — before you've even shipped.

This guide walks through building that page with Tailwind CSS v4.0.2 and React. We'll cover the countdown logic, the email form, the layout, and the visual polish. No fluff, no third-party page builders.

Project Setup: Tailwind v4 and Next.js App Router

If you're starting fresh, spin up a Next.js 15 app and install Tailwind. With Tailwind v4, configuration has shifted — there's no more tailwind.config.js by default. You configure everything through CSS @theme blocks instead.

Install with npm install tailwindcss@4.0.2 @tailwindcss/vite or the PostCSS plugin depending on your setup. For Next.js specifically, use @tailwindcss/postcss. Then import Tailwind in your global CSS file with @import "tailwindcss".

If you haven't read about what changed, check out our Tailwind v4 features breakdown — it covers the new engine, the CSS-first config approach, and the OKLCH color system. Worth 10 minutes of your time before you start a new project.

Building the Countdown Timer Component

The countdown is the heart of the page. You need a launch date, a setInterval that ticks every second, and a bit of math to extract days, hours, minutes, and seconds. Nothing exotic here.

Here's a clean React component that handles it. It uses useEffect for the interval, useState for the time object, and formats each unit with zero-padding. The targetDate is just an ISO string you can drop into a CMS or env variable later.

"use client";
import { useState, useEffect } from "react";

interface TimeLeft {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
}

function getTimeLeft(target: Date): TimeLeft {
  const diff = target.getTime() - Date.now();
  if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
  return {
    days: Math.floor(diff / (1000 * 60 * 60 * 24)),
    hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
    minutes: Math.floor((diff / (1000 * 60)) % 60),
    seconds: Math.floor((diff / 1000) % 60),
  };
}

export function CountdownTimer({ targetDate }: { targetDate: string }) {
  const target = new Date(targetDate);
  const [timeLeft, setTimeLeft] = useState<TimeLeft>(getTimeLeft(target));

  useEffect(() => {
    const id = setInterval(() => setTimeLeft(getTimeLeft(target)), 1000);
    return () => clearInterval(id);
  }, [targetDate]);

  const units = [
    { label: "Days", value: timeLeft.days },
    { label: "Hours", value: timeLeft.hours },
    { label: "Minutes", value: timeLeft.minutes },
    { label: "Seconds", value: timeLeft.seconds },
  ];

  return (
    <div className="flex gap-4 justify-center">
      {units.map(({ label, value }) => (
        <div
          key={label}
          className="flex flex-col items-center bg-white/10 backdrop-blur-md rounded-xl px-6 py-4 min-w-[80px]"
        >
          <span className="text-4xl font-mono font-bold tabular-nums">
            {String(value).padStart(2, "0")}
          </span>
          <span className="text-xs uppercase tracking-widest text-white/60 mt-1">
            {label}
          </span>
        </div>
      ))}
    </div>
  );
}

A few things worth noting. The tabular-nums class prevents the layout from jumping as digits change — it's one of those Tailwind utilities that's easy to forget but makes a real difference. The backdrop-blur-md on each card works great if you've got a gradient or image background behind it, giving that glassmorphism effect that's been popular in dashboards and launch pages alike.

Email Capture Form with Tailwind Styling

The email form should be simple. One input, one button, no distractions. You don't need a name field. You don't need a checkbox for a newsletter you don't have yet. Just the email.

Style the input with a transparent background and a subtle border — something like border border-white/20 bg-white/5 focus:border-white/50 focus:outline-none gives you a clean dark-mode feel without any extra config. Add rounded-lg px-4 py-3 and a placeholder:text-white/40 for the placeholder color. That's it.

For form handling, hook it up to whatever email service you're using — Resend, Mailchimp, ConvertKit. POST to an API route in Next.js and return a success or error state. Keep the UX tight: disable the button while submitting, show a success message inline, don't redirect. Redirect kills the page's SEO value.

Page Layout: Centering Everything Correctly

The classic mistake is using flex items-center justify-center h-screen and calling it done. That works on desktop. On mobile with a keyboard open, your content gets crushed. Use min-h-screen instead, and add py-16 top and bottom so there's always breathing room.

Here's a layout pattern that holds up: flex flex-col items-center justify-center min-h-screen px-4 py-16 text-center. The text-center at the container level means you don't have to repeat it on every child. Then for the inner content wrapper, use max-w-2xl w-full mx-auto flex flex-col gap-10.

The 8px gap system maps cleanly here. gap-10 is 40px — a comfortable vertical rhythm for a page with a headline, subheading, countdown, and form. If sections feel too close or too spread, adjust in 4px increments: gap-8 (32px) or gap-12 (48px). Don't eyeball it, be intentional.

For background, a radial gradient works really well. Something like background: radial-gradient(ellipse at top, rgba(99,102,241,0.15) 0%, transparent 70%) over a dark base gives depth without being busy. You can pair it with a particles background if the page needs more visual life.

Adding a Progress Bar to Show Launch Proximity

A countdown tells you how much time is left. A progress bar shows how far along the launch timeline you already are. Combined, they give visitors a much better sense of momentum — you're not just waiting, you're watching something get built.

Calculate the percentage as (elapsed / total) * 100 where elapsed = Date.now() - startDate.getTime() and total = targetDate.getTime() - startDate.getTime(). Feed that into a Tailwind-styled bar.

function LaunchProgress({ start, end }: { start: string; end: string }) {
  const startMs = new Date(start).getTime();
  const endMs = new Date(end).getTime();
  const now = Date.now();
  const pct = Math.min(100, Math.max(0, ((now - startMs) / (endMs - startMs)) * 100));

  return (
    <div className="w-full max-w-md mx-auto">
      <div className="flex justify-between text-xs text-white/50 mb-2">
        <span>Development started</span>
        <span>{Math.round(pct)}% complete</span>
      </div>
      <div className="h-1.5 w-full bg-white/10 rounded-full overflow-hidden">
        <div
          className="h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full transition-all duration-700"
          style={{ width: `${pct}%` }}
        />
      </div>
    </div>
  );
}

The inline style here is intentional — pct is dynamic, and Tailwind's JIT can't generate arbitrary percentage widths at runtime. Don't try to use a class like w-[${pct}%] inside JSX expressions; that doesn't work. Use the style prop for dynamic numeric values, and reserve Tailwind for everything static.

Dark Mode and Theme Considerations

Coming soon pages almost always look better dark. A light coming soon page reads as unfinished. Dark reads as intentional. That said, you should still wire up theme support correctly from day one — not as an afterthought.

If your main app has theme toggle functionality, reuse the same pattern here. Even if you don't expose the toggle on the coming soon page itself, having the infrastructure in place means you won't need to refactor when you launch.

In Tailwind v4, the dark: variant works differently by default — it uses the prefers-color-scheme media query unless you configure @custom-variant dark (&:where(.dark, .dark *)) in your CSS. Pick one approach and stick to it across the project. Mixing the two leads to style bugs that are painful to trace.

Reusable Patterns and What to Do After Launch

The coming soon page should live at a dedicated route — /coming-soon or sometimes just / if the product isn't live yet. When you launch, redirect /coming-soon to the homepage and keep the route alive as a 301 so you don't lose any backlinks or email traffic.

Extract the countdown and email form as standalone components. They'll reuse on sale pages, feature announcement banners, product drops — all of it. This is exactly the kind of pattern covered in our Tailwind component patterns guide, where building modular pieces pays dividends over time.

What about the email list you collected? Export it before the launch day, get it into your email tool, and send a personal "we're live" message. Not a newsletter blast — an actual message. Conversion rate on warm "we're finally live" emails is dramatically higher than cold traffic.

FAQ

How do I prevent the countdown from showing wrong time on server-side render in Next.js?

Initialize the time state to null and only populate it after mount inside a useEffect. Render a skeleton or empty state on the server, then hydrate the real countdown on the client. This avoids hydration mismatches caused by the server and client having different Date.now() values.

Can I animate the countdown digits with Tailwind CSS alone?

Not for flip animations — Tailwind doesn't include keyframe-based number flip effects out of the box. You can add a transition-all on the wrapper to fade digits, but for a proper slot-machine flip you'll need a small custom CSS animation or a library like react-flip-toolkit. The tabular-nums class in Tailwind v4 at least prevents layout shift.

Where should I store the launch target date — hardcoded or environment variable?

Environment variable, always. NEXT_PUBLIC_LAUNCH_DATE=2027-03-01T00:00:00Z in your .env.local and process.env.NEXT_PUBLIC_LAUNCH_DATE in the component. This means you can push the date without a code deploy, and your staging environment can use a different date for testing.

What's the best way to handle the form submission without a backend?

Use a serverless form service. Resend has a free tier and a REST API that takes about 10 lines of code. Alternatively, Formspree and Buttondown both accept POST requests directly from the browser. For a Next.js app, a Route Handler in app/api/subscribe/route.ts that proxies to your email provider keeps credentials server-side.

My countdown jumps on mobile when seconds update — how do I fix the layout shift?

Add min-w-[4ch] or a fixed w-16 to each digit span, and use font-mono tabular-nums on the text. Proportional fonts cause characters to take different widths — '1' is narrower than '8', so the layout shifts. Monospace + tabular nums locks all digit widths to the same value.

Should the coming soon page be indexed by Google?

Usually no. Add a <meta name='robots' content='noindex' /> tag or return a noindex directive from your Next.js metadata config. You don't want a coming soon page ranking for your brand name when the real site launches — it competes with itself and confuses visitors who land on it after you're live.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

E-Commerce Product Page with Tailwind: Gallery, Options, BuyBlog Template with Tailwind: Typography, TOC, Author BioNeobrutalism Landing Page: Bold, Opinionated, High-ConvertingHero Section Variants: 8 High-Converting Landing Page Designs