Error Coming Soon
Original · freecoming soon page with countdown and email
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"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'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's roadmap, standups, and shipping
cadence in one place. Join the early list and we'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
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 →
Error 404
Originalfriendly 404 page with illustration and CTA

Error 500
Original500 server error page

Error 403
Original403 access denied page

Error Offline
Originaloffline / no-connection state

Error Maintenance
Originalscheduled maintenance page

Error Under Construction
Originalunder construction page

Error Search Empty
Originalno search results page

Error Expired
Originallink/session expired page

Error Generic
Originalgeneric error page with retry

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

