Countdown CTA
Original · freeCTA with a countdown timer
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/ctax-countdown.json"use client";
import { useEffect, useMemo, useState, type FormEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type Plan = "monthly" | "annual";
type TimeLeft = {
days: number;
hours: number;
minutes: number;
seconds: number;
done: boolean;
};
const EMPTY_TIME: TimeLeft = {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
done: false,
};
function computeTimeLeft(target: number): TimeLeft {
const diff = Math.max(0, target - Date.now());
const totalSeconds = Math.floor(diff / 1000);
return {
days: Math.floor(totalSeconds / 86400),
hours: Math.floor((totalSeconds % 86400) / 3600),
minutes: Math.floor((totalSeconds % 3600) / 60),
seconds: totalSeconds % 60,
done: diff === 0,
};
}
function pad(value: number): string {
return String(value).padStart(2, "0");
}
const PLANS: Record<Plan, { price: number; note: string; savings: string | null }> = {
monthly: { price: 29, note: "per member · billed monthly", savings: null },
annual: { price: 19, note: "per member · billed yearly", savings: "Save 34%" },
};
const SEATS_TOTAL = 500;
const SEATS_CLAIMED = 363;
export default function CtaxCountdown() {
const reduce = useReducedMotion();
const [deadline, setDeadline] = useState<number | null>(null);
const [timeLeft, setTimeLeft] = useState<TimeLeft>(EMPTY_TIME);
const [plan, setPlan] = useState<Plan>("annual");
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
// Set the deadline on the client only, to avoid any server/client mismatch.
useEffect(() => {
const target =
Date.now() +
2 * 24 * 60 * 60 * 1000 +
11 * 60 * 60 * 1000 +
42 * 60 * 1000 +
18 * 1000;
setDeadline(target);
}, []);
useEffect(() => {
if (deadline === null) return;
const tick = () => setTimeLeft(computeTimeLeft(deadline));
tick();
const id = window.setInterval(tick, 1000);
return () => window.clearInterval(id);
}, [deadline]);
const units = useMemo(
() => [
{ key: "days", label: "Days", value: timeLeft.days },
{ key: "hours", label: "Hours", value: timeLeft.hours },
{ key: "minutes", label: "Minutes", value: timeLeft.minutes },
{ key: "seconds", label: "Seconds", value: timeLeft.seconds },
],
[timeLeft],
);
const seatsLeft = Math.max(0, SEATS_TOTAL - SEATS_CLAIMED);
const claimedPct = Math.round((SEATS_CLAIMED / SEATS_TOTAL) * 100);
const active = PLANS[plan];
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const value = email.trim();
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
if (!valid) {
setSubmitted(false);
setError("Enter a valid email so we can hold your founding seat.");
return;
}
setError(null);
setSubmitted(true);
};
const rise = (delay: number) =>
reduce
? { initial: false as const }
: {
initial: { opacity: 0, y: 22 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.55, ease: "easeOut" as const, delay },
};
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 sm:px-6 sm:py-28 lg:py-32 dark:bg-slate-950">
<style>{`
@keyframes ctax-float {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(0, -22px, 0) scale(1.05); }
}
@keyframes ctax-glow {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.7; }
}
@keyframes ctax-tick {
0% { opacity: 0; transform: translateY(-42%); }
60% { opacity: 1; }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes ctax-sheen {
0% { background-position: -160% 0; }
100% { background-position: 160% 0; }
}
.ctax-blob { animation: ctax-float 11s ease-in-out infinite; }
.ctax-blob-slow { animation: ctax-float 15s ease-in-out infinite; animation-delay: -4s; }
.ctax-glow { animation: ctax-glow 6s ease-in-out infinite; }
.ctax-tick-num { animation: ctax-tick 0.5s cubic-bezier(0.22, 1, 0.36, 1); }
.ctax-sheen {
background-image: linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.55) 50%, transparent 70%);
background-size: 200% 100%;
animation: ctax-sheen 3.2s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.ctax-blob, .ctax-blob-slow, .ctax-glow, .ctax-tick-num, .ctax-sheen {
animation: none !important;
}
}
`}</style>
{/* Decorative background */}
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="ctax-blob absolute -left-24 -top-24 h-72 w-72 rounded-full bg-gradient-to-br from-indigo-400/40 to-violet-500/30 blur-3xl dark:from-indigo-600/30 dark:to-violet-700/20" />
<div className="ctax-blob-slow absolute -bottom-32 right-[-6rem] h-80 w-80 rounded-full bg-gradient-to-br from-sky-400/30 to-indigo-500/25 blur-3xl dark:from-sky-600/20 dark:to-indigo-700/20" />
<div className="ctax-glow absolute left-1/2 top-1/3 h-64 w-64 -translate-x-1/2 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-600/15" />
</div>
<div className="relative mx-auto max-w-5xl">
<motion.div
{...rise(0)}
className="overflow-hidden rounded-3xl border border-slate-200 bg-white/80 shadow-2xl shadow-slate-900/5 backdrop-blur-xl dark:border-slate-800 dark:bg-slate-900/70 dark:shadow-black/40"
>
{/* Top accent bar */}
<div className="h-1.5 w-full bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-500" />
<div className="px-6 py-10 sm:px-10 sm:py-12 lg:px-14 lg:py-14">
{/* Eyebrow */}
<motion.div {...rise(0.05)} className="flex justify-center">
<span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-4 py-1.5 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
<svg
viewBox="0 0 24 24"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8Z" />
</svg>
Founding member pricing
</span>
</motion.div>
{/* Headline */}
<motion.h2
{...rise(0.1)}
className="mx-auto mt-6 max-w-3xl text-center text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-white"
>
Lock in launch pricing before{" "}
<span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-sky-600 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-sky-400">
the timer hits zero
</span>
</motion.h2>
<motion.p
{...rise(0.15)}
className="mx-auto mt-4 max-w-2xl text-center text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-300"
>
Aster brings your docs, tasks, and dashboards into one calm workspace. The
first {SEATS_TOTAL} founding members keep this rate for life — no renewals
creep, no surprise increases.
</motion.p>
{/* Countdown */}
<motion.div
{...rise(0.2)}
className="mx-auto mt-10 grid max-w-xl grid-cols-4 gap-2.5 sm:gap-4"
role="timer"
aria-label={`Offer ends in ${timeLeft.days} days, ${timeLeft.hours} hours, ${timeLeft.minutes} minutes and ${timeLeft.seconds} seconds`}
>
{units.map((unit) => (
<div
key={unit.key}
className="relative flex flex-col items-center rounded-2xl border border-slate-200 bg-gradient-to-b from-white to-slate-100 px-1 py-4 shadow-sm dark:border-slate-700/70 dark:from-slate-800 dark:to-slate-900"
>
{unit.key === "seconds" ? (
<span
key={timeLeft.seconds}
className="ctax-tick-num inline-block bg-gradient-to-b from-indigo-600 to-violet-600 bg-clip-text text-3xl font-bold tabular-nums text-transparent sm:text-5xl dark:from-indigo-300 dark:to-violet-300"
>
{pad(unit.value)}
</span>
) : (
<span className="text-3xl font-bold tabular-nums text-slate-900 sm:text-5xl dark:text-white">
{pad(unit.value)}
</span>
)}
<span className="mt-1.5 text-[0.6rem] font-semibold uppercase tracking-widest text-slate-500 sm:text-xs dark:text-slate-400">
{unit.label}
</span>
</div>
))}
</motion.div>
{timeLeft.done && deadline !== null && (
<p
className="mt-4 text-center text-sm font-semibold text-rose-600 dark:text-rose-400"
role="status"
>
Founding enrollment has closed — join the waitlist for the next cohort.
</p>
)}
{/* Plan toggle */}
<motion.div {...rise(0.25)} className="mt-10 flex flex-col items-center">
<div
className="inline-flex items-center rounded-full border border-slate-200 bg-slate-100 p-1 dark:border-slate-700 dark:bg-slate-800"
role="group"
aria-label="Choose a billing period"
>
<button
type="button"
onClick={() => setPlan("monthly")}
aria-pressed={plan === "monthly"}
className={`rounded-full px-5 py-2 text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
plan === "monthly"
? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
Monthly
</button>
<button
type="button"
onClick={() => setPlan("annual")}
aria-pressed={plan === "annual"}
className={`flex items-center gap-2 rounded-full px-5 py-2 text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
plan === "annual"
? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
Yearly
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[0.65rem] font-bold text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300">
-34%
</span>
</button>
</div>
{/* Price */}
<div className="mt-6 flex items-end justify-center gap-2">
<span className="text-lg font-medium text-slate-400 line-through dark:text-slate-500">
$49
</span>
<span className="text-5xl font-bold tracking-tight text-slate-900 tabular-nums dark:text-white">
${active.price}
</span>
<span className="mb-1.5 text-sm text-slate-500 dark:text-slate-400">/mo</span>
</div>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
{active.note}
{active.savings ? (
<span className="ml-2 font-semibold text-emerald-600 dark:text-emerald-400">
{active.savings}
</span>
) : null}
</p>
</motion.div>
{/* Email capture form */}
<motion.form
{...rise(0.3)}
onSubmit={handleSubmit}
noValidate
className="mx-auto mt-8 max-w-lg"
>
<div className="flex flex-col gap-3 sm:flex-row">
<label htmlFor="ctax-email" className="sr-only">
Work email
</label>
<input
id="ctax-email"
type="email"
name="email"
autoComplete="email"
value={email}
onChange={(event) => {
setEmail(event.target.value);
if (error) setError(null);
}}
placeholder="you@company.com"
aria-invalid={error ? true : undefined}
aria-describedby={error ? "ctax-email-error" : undefined}
className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-950 dark:text-white dark:placeholder:text-slate-500"
/>
<button
type="submit"
className="ctax-sheen inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition-transform hover:-translate-y-0.5 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 active:translate-y-0 dark:shadow-indigo-900/40"
>
Claim founding rate
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={2.2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</button>
</div>
<div aria-live="polite" className="min-h-[1.25rem]">
{error ? (
<p
id="ctax-email-error"
className="mt-2 text-sm font-medium text-rose-600 dark:text-rose-400"
>
{error}
</p>
) : submitted ? (
<p className="mt-2 flex items-center justify-center gap-1.5 text-sm font-medium text-emerald-600 dark:text-emerald-400">
<svg
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M20 6 9 17l-5-5" />
</svg>
Seat reserved — check {email} for your founding link.
</p>
) : null}
</div>
</motion.form>
{/* Scarcity bar */}
<motion.div {...rise(0.35)} className="mx-auto mt-6 max-w-lg">
<div className="mb-1.5 flex items-center justify-between text-xs font-medium text-slate-500 dark:text-slate-400">
<span>
<span className="font-bold text-slate-900 dark:text-white">
{SEATS_CLAIMED}
</span>{" "}
of {SEATS_TOTAL} founding seats claimed
</span>
<span className="font-semibold text-indigo-600 dark:text-indigo-400">
{seatsLeft} left
</span>
</div>
<div
className="h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
role="progressbar"
aria-valuenow={claimedPct}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Founding seats claimed"
>
<div
className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-500"
style={{ width: `${claimedPct}%` }}
/>
</div>
</motion.div>
{/* Trust row */}
<motion.ul
{...rise(0.4)}
className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-3 text-sm text-slate-600 dark:text-slate-400"
>
{["Cancel anytime", "No card for the 14-day trial", "Rate locked for life"].map(
(item) => (
<li key={item} className="flex items-center gap-1.5">
<svg
viewBox="0 0 24 24"
className="h-4 w-4 text-emerald-500 dark:text-emerald-400"
fill="none"
stroke="currentColor"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M20 6 9 17l-5-5" />
</svg>
{item}
</li>
),
)}
</motion.ul>
</div>
</motion.div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Gradient CTA
OriginalA bold gradient call-to-action banner with two buttons.

Full-Bleed Dark CTA
OriginalA full-bleed dark call-to-action banner with a live status badge, subtle grid backdrop and a primary plus secondary button pair to drive discovery calls.

Split CTA With Lead Form
OriginalA two-column call-to-action that pairs a benefit checklist with an inline name, email and message form for capturing qualified leads directly in the section.

Newsletter CTA With Stats
OriginalA newsletter sign-up call-to-action combining an inline email subscribe form with a three-cell social-proof stat strip to boost conversions.

Aurora Glow CTA Banner
OriginalA full-bleed dark call-to-action banner with a rotating aurora gradient, floating orbs and a shine-on-hover button, with staggered blur-in text that reveals on scroll.

Animated Border Beam CTA
OriginalAn email-capture call-to-action card wrapped in a rotating conic-gradient border beam, with staggered reveal, animated shine text and an inline success state on submit.

Marquee Ribbon CTA
OriginalA dark call-to-action band framed by two infinite scrolling tag ribbons with a continuously flowing animated-gradient button and staggered centre content.

Centered CTA
Originalcentred CTA band

Split CTA
Originalsplit CTA with image side

Gradient Band CTA
Originalgradient CTA band

Newsletter CTA
OriginalCTA with newsletter input

App Download CTA
Originalapp download CTA with store buttons

