Web InnoventixFreeCode

Error Coming Soon

Original · free

coming soon page with countdown and email

byWeb InnoventixReact + Tailwind
errorcomingsoonpages
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/error-coming-soon.json
error-coming-soon.tsx
"use client";

import {
  useState,
  useEffect,
  useRef,
  useMemo,
  type FormEvent,
  type ChangeEvent,
} from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";

type TimeLeft = {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
};

type FormStatus = "idle" | "success" | "error";

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function computeTimeLeft(target: number): TimeLeft {
  const diff = Math.max(0, target - Date.now());
  const total = Math.floor(diff / 1000);
  return {
    days: Math.floor(total / 86400),
    hours: Math.floor((total % 86400) / 3600),
    minutes: Math.floor((total % 3600) / 60),
    seconds: total % 60,
  };
}

function pad2(n: number): string {
  return String(Math.max(0, n)).padStart(2, "0");
}

function RocketIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-6 w-6"
      aria-hidden="true"
    >
      <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z" />
      <path d="M12 15l-3-3a22 22 0 0 1 8-10c3 0 5 2 5 5a22 22 0 0 1-10 8Z" />
      <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
      <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
    </svg>
  );
}

function MailIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.6}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-5 w-5"
      aria-hidden="true"
    >
      <rect x="2.5" y="4.5" width="19" height="15" rx="2.5" />
      <path d="m3.5 7 8.5 6 8.5-6" />
    </svg>
  );
}

function ArrowIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-4 w-4"
      aria-hidden="true"
    >
      <path d="M5 12h14" />
      <path d="m13 6 6 6-6 6" />
    </svg>
  );
}

function CheckIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      className="h-5 w-5"
      aria-hidden="true"
    >
      <path d="m4 12.5 5 5 11-11" />
    </svg>
  );
}

type Social = { label: string; href: string; path: string };

const SOCIALS: Social[] = [
  {
    label: "Halo on X",
    href: "https://x.com/halohq",
    path: "M18.24 2.25h3.31l-7.23 8.26 8.5 11.24h-6.66l-5.22-6.82-5.97 6.82H1.66l7.73-8.84L1.24 2.25h6.83l4.71 6.23 5.46-6.23Zm-1.16 17.52h1.83L7.01 4.13H5.05l12.03 15.64Z",
  },
  {
    label: "Halo on GitHub",
    href: "https://github.com/halohq",
    path: "M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.09-.65.35-1.08.63-1.33-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.26-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.39.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z",
  },
  {
    label: "Halo on LinkedIn",
    href: "https://www.linkedin.com/company/halohq",
    path: "M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28ZM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14ZM7.12 20.45H3.55V9h3.57v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0Z",
  },
];

const PROGRESS = 82;

export default function ComingSoonPage() {
  const reduce = useReducedMotion();

  const targetRef = useRef<number | null>(null);
  if (targetRef.current === null) {
    targetRef.current =
      Date.now() +
      (46 * 24 * 60 * 60 + 8 * 3600 + 23 * 60 + 12) * 1000;
  }
  const target = targetRef.current;

  const [timeLeft, setTimeLeft] = useState<TimeLeft>(() =>
    computeTimeLeft(target),
  );
  const [email, setEmail] = useState("");
  const [status, setStatus] = useState<FormStatus>("idle");
  const [message, setMessage] = useState("");

  useEffect(() => {
    const id = window.setInterval(() => {
      setTimeLeft(computeTimeLeft(target));
    }, 1000);
    return () => window.clearInterval(id);
  }, [target]);

  const launchLabel = useMemo(
    () =>
      new Intl.DateTimeFormat("en-US", {
        weekday: "long",
        month: "long",
        day: "numeric",
        year: "numeric",
      }).format(new Date(target)),
    [target],
  );

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

  function handleChange(e: ChangeEvent<HTMLInputElement>) {
    setEmail(e.target.value);
    if (status === "error") {
      setStatus("idle");
      setMessage("");
    }
  }

  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const value = email.trim();
    if (!EMAIL_RE.test(value)) {
      setStatus("error");
      setMessage("That email doesn't look right — check for typos and try again.");
      return;
    }
    setStatus("success");
    setMessage("You're on the list. We'll send your early invite the moment doors open.");
  }

  const fade = reduce
    ? {}
    : {
        initial: { opacity: 0, y: 18 },
        animate: { opacity: 1, y: 0 },
      };

  return (
    <section className="relative w-full min-h-screen overflow-hidden bg-slate-50 px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-28">
      <style>{`
        @keyframes ecs-aurora-a {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(6%, -4%, 0) scale(1.18); }
        }
        @keyframes ecs-aurora-b {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1.1); }
          50% { transform: translate3d(-5%, 5%, 0) scale(1); }
        }
        @keyframes ecs-aurora-c {
          0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
          50% { transform: translate3d(4%, 6%, 0) scale(1.22); }
        }
        @keyframes ecs-grid-pan {
          0% { background-position: 0 0; }
          100% { background-position: 46px 46px; }
        }
        @keyframes ecs-pulse-ring {
          0% { transform: scale(0.85); opacity: 0.6; }
          70% { transform: scale(1.9); opacity: 0; }
          100% { transform: scale(1.9); opacity: 0; }
        }
        @keyframes ecs-shimmer {
          0% { background-position: -160% 0; }
          100% { background-position: 260% 0; }
        }
        .ecs-aurora-a { animation: ecs-aurora-a 19s ease-in-out infinite; }
        .ecs-aurora-b { animation: ecs-aurora-b 23s ease-in-out infinite; }
        .ecs-aurora-c { animation: ecs-aurora-c 27s ease-in-out infinite; }
        .ecs-grid-pan { animation: ecs-grid-pan 24s linear infinite; }
        .ecs-pulse-ring { animation: ecs-pulse-ring 3.4s ease-out infinite; }
        .ecs-shimmer {
          background-size: 200% 100%;
          animation: ecs-shimmer 2.8s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .ecs-aurora-a,
          .ecs-aurora-b,
          .ecs-aurora-c,
          .ecs-grid-pan,
          .ecs-pulse-ring,
          .ecs-shimmer {
            animation: none !important;
          }
        }
      `}</style>

      {/* Background layers */}
      <div className="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
        <div className="ecs-aurora-a absolute -left-24 -top-24 h-[28rem] w-[28rem] rounded-full bg-indigo-400/40 blur-3xl dark:bg-indigo-600/30" />
        <div className="ecs-aurora-b absolute -right-28 top-10 h-[26rem] w-[26rem] rounded-full bg-violet-400/40 blur-3xl dark:bg-violet-600/30" />
        <div className="ecs-aurora-c absolute bottom-[-8rem] left-1/3 h-[30rem] w-[30rem] rounded-full bg-sky-400/30 blur-3xl dark:bg-sky-600/25" />
        <div
          className="ecs-grid-pan absolute inset-0 opacity-[0.35] dark:opacity-[0.22]"
          style={{
            backgroundImage:
              "linear-gradient(to right, rgb(100 116 139 / 0.16) 1px, transparent 1px), linear-gradient(to bottom, rgb(100 116 139 / 0.16) 1px, transparent 1px)",
            backgroundSize: "46px 46px",
            maskImage:
              "radial-gradient(ellipse 70% 55% at 50% 42%, black 40%, transparent 100%)",
            WebkitMaskImage:
              "radial-gradient(ellipse 70% 55% at 50% 42%, black 40%, transparent 100%)",
          }}
        />
      </div>

      <div className="relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center text-center">
        {/* Badge + emblem */}
        <motion.div
          {...fade}
          transition={{ duration: reduce ? 0 : 0.5 }}
          className="flex flex-col items-center"
        >
          <div className="relative mb-6 grid h-16 w-16 place-items-center rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-500/30 dark:shadow-indigo-900/40">
            <span className="ecs-pulse-ring absolute inset-0 rounded-2xl ring-2 ring-indigo-400/70" aria-hidden="true" />
            <RocketIcon />
          </div>

          <span className="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-white/70 px-4 py-1.5 text-xs font-medium tracking-wide text-slate-600 backdrop-blur dark:border-slate-700/70 dark:bg-slate-900/60 dark:text-slate-300">
            <span className="relative flex h-2 w-2">
              <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75 motion-reduce:hidden" />
              <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
            </span>
            Private beta · Launching this fall
          </span>
        </motion.div>

        {/* Heading */}
        <motion.h1
          {...fade}
          transition={{ duration: reduce ? 0 : 0.5, delay: reduce ? 0 : 0.08 }}
          className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-6xl"
        >
          Something new is on the{" "}
          <span className="bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-500 bg-clip-text text-transparent">
            horizon
          </span>
          .
        </motion.h1>

        <motion.p
          {...fade}
          transition={{ duration: reduce ? 0 : 0.5, delay: reduce ? 0 : 0.16 }}
          className="mt-5 max-w-xl text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400 sm:text-lg"
        >
          We&apos;re putting the final touches on{" "}
          <span className="font-semibold text-slate-800 dark:text-slate-200">Halo</span>{" "}
          — a calmer way to run your team&apos;s roadmap, standups, and shipping
          cadence in one place. Join the early list and we&apos;ll send your invite
          the moment doors open.
        </motion.p>

        {/* Countdown */}
        <motion.div
          {...fade}
          transition={{ duration: reduce ? 0 : 0.5, delay: reduce ? 0 : 0.24 }}
          className="mt-10 w-full"
        >
          <div
            role="timer"
            aria-label={`Time remaining until launch on ${launchLabel}`}
            className="grid grid-cols-4 gap-2.5 sm:gap-4"
          >
            <p className="sr-only">
              {timeLeft.days} days, {timeLeft.hours} hours, {timeLeft.minutes}{" "}
              minutes and {timeLeft.seconds} seconds until launch.
            </p>
            {units.map((unit) => (
              <div
                key={unit.label}
                className="flex flex-col items-center rounded-2xl border border-slate-200/80 bg-white/70 px-1 py-4 shadow-sm backdrop-blur dark:border-slate-800 dark:bg-slate-900/60 sm:py-5"
              >
                <div
                  className="relative flex h-12 w-full items-center justify-center overflow-hidden sm:h-16"
                  aria-hidden="true"
                >
                  <AnimatePresence initial={false} mode="popLayout">
                    <motion.span
                      key={pad2(unit.value)}
                      initial={reduce ? false : { y: "-90%", opacity: 0 }}
                      animate={{ y: "0%", opacity: 1 }}
                      exit={reduce ? { opacity: 0 } : { y: "90%", opacity: 0 }}
                      transition={{
                        duration: reduce ? 0 : 0.38,
                        ease: [0.22, 1, 0.36, 1],
                      }}
                      className="text-3xl font-semibold tabular-nums tracking-tight text-slate-900 dark:text-white sm:text-5xl"
                    >
                      {pad2(unit.value)}
                    </motion.span>
                  </AnimatePresence>
                </div>
                <span className="mt-1.5 text-[0.62rem] font-medium uppercase tracking-[0.15em] text-slate-500 dark:text-slate-400 sm:text-xs">
                  {unit.label}
                </span>
              </div>
            ))}
          </div>
          <p className="mt-3 text-xs text-slate-500 dark:text-slate-400">
            Doors open{" "}
            <span className="font-medium text-slate-700 dark:text-slate-300">
              {launchLabel}
            </span>
          </p>
        </motion.div>

        {/* Progress */}
        <motion.div
          {...fade}
          transition={{ duration: reduce ? 0 : 0.5, delay: reduce ? 0 : 0.3 }}
          className="mt-8 w-full max-w-md"
        >
          <div className="mb-2 flex items-center justify-between text-xs font-medium text-slate-500 dark:text-slate-400">
            <span>Build progress</span>
            <span className="tabular-nums text-slate-700 dark:text-slate-300">
              {PROGRESS}%
            </span>
          </div>
          <div
            role="progressbar"
            aria-label="Product build progress"
            aria-valuenow={PROGRESS}
            aria-valuemin={0}
            aria-valuemax={100}
            className="h-2.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
          >
            <div
              className="ecs-shimmer h-full rounded-full bg-[linear-gradient(90deg,#6366f1,#8b5cf6,#38bdf8,#8b5cf6,#6366f1)]"
              style={{ width: `${PROGRESS}%` }}
            />
          </div>
        </motion.div>

        {/* Email capture */}
        <motion.div
          {...fade}
          transition={{ duration: reduce ? 0 : 0.5, delay: reduce ? 0 : 0.36 }}
          className="mt-10 w-full max-w-md"
        >
          {status === "success" ? (
            <div
              role="status"
              aria-live="polite"
              className="flex items-start gap-3 rounded-2xl border border-emerald-300/70 bg-emerald-50/80 px-4 py-4 text-left text-sm text-emerald-800 backdrop-blur dark:border-emerald-800/70 dark:bg-emerald-950/40 dark:text-emerald-200"
            >
              <span className="mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full bg-emerald-500 text-white">
                <CheckIcon />
              </span>
              <span>{message}</span>
            </div>
          ) : (
            <form onSubmit={handleSubmit} noValidate className="w-full">
              <label htmlFor="ecs-email" className="sr-only">
                Email address
              </label>
              <div className="flex flex-col gap-2.5 sm:flex-row">
                <div className="relative flex-1">
                  <span className="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500">
                    <MailIcon />
                  </span>
                  <input
                    id="ecs-email"
                    type="email"
                    inputMode="email"
                    autoComplete="email"
                    value={email}
                    onChange={handleChange}
                    placeholder="you@company.com"
                    aria-invalid={status === "error"}
                    aria-describedby={status === "error" ? "ecs-form-msg" : undefined}
                    className={`w-full rounded-xl border bg-white/80 py-3 pl-11 pr-4 text-sm text-slate-900 shadow-sm outline-none backdrop-blur transition placeholder:text-slate-400 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:bg-slate-900/70 dark:text-white dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-950 ${
                      status === "error"
                        ? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-700"
                        : "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700"
                    }`}
                  />
                </div>
                <button
                  type="submit"
                  className="inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-500 to-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-md shadow-indigo-500/30 outline-none transition hover:from-indigo-600 hover:to-violet-700 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 active:scale-[0.98] dark:shadow-indigo-900/40 dark:focus-visible:ring-offset-slate-950"
                >
                  Notify me
                  <ArrowIcon />
                </button>
              </div>
              <div id="ecs-form-msg" role="status" aria-live="polite" className="min-h-[1.25rem]">
                {status === "error" && (
                  <p className="mt-2 text-left text-xs font-medium text-rose-600 dark:text-rose-400">
                    {message}
                  </p>
                )}
              </div>
              <p className="mt-1 text-left text-xs text-slate-400 dark:text-slate-500">
                No spam — one email when we launch, and nothing else.
              </p>
            </form>
          )}
        </motion.div>

        {/* Socials */}
        <motion.div
          {...fade}
          transition={{ duration: reduce ? 0 : 0.5, delay: reduce ? 0 : 0.42 }}
          className="mt-12 flex items-center gap-3"
        >
          {SOCIALS.map((s) => (
            <a
              key={s.label}
              href={s.href}
              target="_blank"
              rel="noopener"
              aria-label={s.label}
              className="grid h-10 w-10 place-items-center rounded-full border border-slate-300/80 bg-white/70 text-slate-600 outline-none backdrop-blur transition hover:-translate-y-0.5 hover:border-indigo-400 hover:text-indigo-600 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900/60 dark:text-slate-400 dark:hover:border-indigo-500 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-950"
            >
              <svg viewBox="0 0 24 24" fill="currentColor" className="h-4 w-4" aria-hidden="true">
                <path d={s.path} />
              </svg>
            </a>
          ))}
        </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 →