Web InnoventixFreeCode

Countdown CTA

Original · free

CTA with a countdown timer

byWeb InnoventixReact + Tailwind
ctaxcountdowncta
Open

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
ctax-countdown.tsx
"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

motion

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 quote

Similar components

Browse all →