Error Maintenance
Original · freescheduled maintenance page
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-maintenance.json"use client";
import { useEffect, useId, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type TaskStatus = "done" | "active" | "pending";
interface Task {
id: string;
label: string;
detail: string;
status: TaskStatus;
}
const TASKS: Task[] = [
{
id: "db",
label: "Database migration to PostgreSQL 16",
detail: "Schema upgraded, 4.2M rows verified",
status: "done",
},
{
id: "search",
label: "Rebuilding search & vector indexes",
detail: "12 indexes reindexed and validated",
status: "done",
},
{
id: "cdn",
label: "Warming CDN cache across 14 regions",
detail: "9 of 14 regions live",
status: "active",
},
{
id: "smoke",
label: "Final health checks & smoke tests",
detail: "Queued — starts after cache warm-up",
status: "pending",
},
];
const STATUS_META: Record<
TaskStatus,
{ word: string; ring: string; text: string; dot: string }
> = {
done: {
word: "Complete",
ring: "ring-emerald-500/25 bg-emerald-50 dark:bg-emerald-500/10",
text: "text-emerald-700 dark:text-emerald-300",
dot: "bg-emerald-500",
},
active: {
word: "In progress",
ring: "ring-amber-500/25 bg-amber-50 dark:bg-amber-500/10",
text: "text-amber-700 dark:text-amber-300",
dot: "bg-amber-500",
},
pending: {
word: "Pending",
ring: "ring-slate-400/20 bg-slate-100 dark:bg-slate-800/60",
text: "text-slate-500 dark:text-slate-400",
dot: "bg-slate-400 dark:bg-slate-500",
},
};
function Gear({
size,
teeth,
spin,
tone,
}: {
size: number;
teeth: number;
spin: "em-gear-a" | "em-gear-b";
tone: "indigo" | "violet";
}) {
const arr = Array.from({ length: teeth }, (_, i) => i);
const toothFill =
tone === "indigo"
? "fill-indigo-500 dark:fill-indigo-400"
: "fill-violet-500 dark:fill-violet-400";
const bodyFill =
tone === "indigo"
? "fill-indigo-400/90 dark:fill-indigo-500/80"
: "fill-violet-400/90 dark:fill-violet-500/80";
const holeFill = "fill-slate-50 dark:fill-slate-950";
return (
<svg
viewBox="0 0 100 100"
width={size}
height={size}
aria-hidden="true"
className={spin}
style={{ transformOrigin: "50% 50%" }}
>
{arr.map((i) => {
const angle = (360 / teeth) * i;
return (
<rect
key={i}
x={44}
y={3}
width={12}
height={17}
rx={2.5}
transform={`rotate(${angle} 50 50)`}
className={toothFill}
/>
);
})}
<circle cx={50} cy={50} r={34} className={bodyFill} />
<circle cx={50} cy={50} r={13} className={holeFill} />
<circle
cx={50}
cy={50}
r={13}
fill="none"
strokeWidth={2}
className="stroke-white/60 dark:stroke-white/10"
/>
</svg>
);
}
function StatusIcon({ status }: { status: TaskStatus }) {
if (status === "done") {
return (
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
<circle cx={12} cy={12} r={11} className="fill-emerald-500" />
<path
d="M7 12.5l3.2 3.2L17 8.8"
fill="none"
stroke="white"
strokeWidth={2.2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
if (status === "active") {
return (
<svg
viewBox="0 0 24 24"
className="em-spin-ring h-5 w-5"
aria-hidden="true"
style={{ transformOrigin: "50% 50%" }}
>
<circle
cx={12}
cy={12}
r={9}
fill="none"
strokeWidth={2.4}
className="stroke-amber-500/25"
/>
<path
d="M12 3a9 9 0 0 1 9 9"
fill="none"
strokeWidth={2.4}
strokeLinecap="round"
className="stroke-amber-500"
/>
</svg>
);
}
return (
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
<circle
cx={12}
cy={12}
r={9}
fill="none"
strokeWidth={2.2}
strokeDasharray="3 3.5"
className="stroke-slate-400 dark:stroke-slate-500"
/>
</svg>
);
}
const fmt = (n: number): string => String(n).padStart(2, "0");
export default function ErrorMaintenance() {
const reduce = useReducedMotion();
const uid = useId();
const headingId = `${uid}-heading`;
const emailFieldId = `${uid}-email`;
const emailErrId = `${uid}-email-err`;
const emailOkId = `${uid}-email-ok`;
const [remaining, setRemaining] = useState<number>(1 * 3600 + 34 * 60 + 12);
const [progress, setProgress] = useState<number>(62);
const [sinceCheck, setSinceCheck] = useState<number>(6);
const [checking, setChecking] = useState<boolean>(false);
const [email, setEmail] = useState<string>("");
const [emailState, setEmailState] = useState<"idle" | "error" | "success">(
"idle"
);
const checkTimer = useRef<number | null>(null);
useEffect(() => {
const tick = window.setInterval(() => {
setRemaining((r) => (r > 0 ? r - 1 : 0));
setSinceCheck((s) => s + 1);
}, 1000);
return () => window.clearInterval(tick);
}, []);
useEffect(() => {
const creep = window.setInterval(() => {
setProgress((p) => (p < 78 ? Math.min(78, p + 1) : p));
}, 3200);
return () => window.clearInterval(creep);
}, []);
useEffect(() => {
return () => {
if (checkTimer.current !== null) window.clearTimeout(checkTimer.current);
};
}, []);
const onRefresh = () => {
if (checking) return;
setChecking(true);
checkTimer.current = window.setTimeout(() => {
setChecking(false);
setSinceCheck(0);
}, 900);
};
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
setEmailState(ok ? "success" : "error");
};
const hours = Math.floor(remaining / 3600);
const mins = Math.floor((remaining % 3600) / 60);
const secs = remaining % 60;
const units: { v: number; label: string }[] = [
{ v: hours, label: "Hours" },
{ v: mins, label: "Minutes" },
{ v: secs, label: "Seconds" },
];
const checkedLabel =
sinceCheck < 5
? "just now"
: sinceCheck < 60
? `${sinceCheck}s ago`
: `${Math.floor(sinceCheck / 60)}m ago`;
const appear = (delay: number) =>
reduce
? {}
: {
initial: { opacity: 0, y: 18 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5, delay, ease: "easeOut" as const },
};
const focusRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";
return (
<section
aria-labelledby={headingId}
className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 text-slate-900 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 dark:text-slate-100"
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-70 dark:opacity-50"
style={{
backgroundImage:
"radial-gradient(circle at 1px 1px, rgba(99,102,241,0.14) 1px, transparent 0)",
backgroundSize: "26px 26px",
maskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%)",
WebkitMaskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%)",
}}
/>
<div
aria-hidden="true"
className="em-float pointer-events-none absolute left-1/2 top-[-6rem] h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-600/20"
/>
<div className="relative mx-auto w-full max-w-3xl px-6 py-20 sm:py-28">
{/* Status badge */}
<motion.div {...appear(0)} className="flex justify-center">
<span className="inline-flex items-center gap-2 rounded-full border border-amber-500/30 bg-amber-50 px-4 py-1.5 text-sm font-medium text-amber-700 dark:border-amber-400/25 dark:bg-amber-500/10 dark:text-amber-300">
<span className="relative flex h-2.5 w-2.5">
<span className="em-ping absolute inline-flex h-full w-full rounded-full bg-amber-500" />
<span className="em-dot relative inline-flex h-2.5 w-2.5 rounded-full bg-amber-500" />
</span>
Scheduled maintenance in progress
</span>
</motion.div>
{/* Gears */}
<motion.div
{...appear(0.05)}
className="relative mx-auto mt-10 flex h-40 w-56 items-center justify-center"
aria-hidden="true"
>
<div className="absolute left-2 top-3">
<Gear size={112} teeth={12} spin="em-gear-a" tone="indigo" />
</div>
<div className="absolute bottom-2 right-4">
<Gear size={78} teeth={10} spin="em-gear-b" tone="violet" />
</div>
</motion.div>
{/* Heading */}
<motion.h1
{...appear(0.1)}
id={headingId}
className="mt-4 text-balance text-center text-4xl font-semibold tracking-tight sm:text-5xl"
>
We’ll be right back.
</motion.h1>
<motion.p
{...appear(0.15)}
className="mx-auto mt-4 max-w-xl text-pretty text-center text-base leading-relaxed text-slate-600 dark:text-slate-400 sm:text-lg"
>
We’re rolling out infrastructure upgrades to make everything
faster and more reliable. Your data is safe and no action is needed on
your end.
</motion.p>
{/* Maintenance window */}
<motion.div
{...appear(0.2)}
className="mt-6 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-slate-500 dark:text-slate-400"
>
<span className="inline-flex items-center gap-1.5">
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
<rect
x={3}
y={5}
width={18}
height={16}
rx={2.5}
fill="none"
strokeWidth={1.8}
className="stroke-slate-400 dark:stroke-slate-500"
/>
<path
d="M3 9h18M8 3v4M16 3v4"
strokeWidth={1.8}
strokeLinecap="round"
className="stroke-slate-400 dark:stroke-slate-500"
/>
</svg>
Window: Sat, Jul 18 · 02:00–04:30 UTC
</span>
<span className="inline-flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-slate-400 dark:bg-slate-500" />
Incident #MW-2048
</span>
</motion.div>
{/* Card: countdown + progress + tasks */}
<motion.div
{...appear(0.28)}
className="mt-10 rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/60 sm:p-8"
>
{/* Countdown */}
<div className="text-center">
<p className="text-xs font-semibold uppercase tracking-widest text-slate-500 dark:text-slate-400">
Estimated time remaining
</p>
{remaining > 0 ? (
<div
role="timer"
aria-label={`Estimated time remaining: ${hours} hours, ${mins} minutes, ${secs} seconds`}
className="mt-4 flex items-stretch justify-center gap-2 sm:gap-3"
>
{units.map((u, i) => (
<div key={u.label} className="flex items-center gap-2 sm:gap-3">
<div className="min-w-[74px] rounded-2xl border border-slate-200 bg-slate-50 px-2 py-3 dark:border-slate-800 dark:bg-slate-950/50 sm:min-w-[88px]">
<div className="bg-gradient-to-b from-slate-900 to-slate-600 bg-clip-text text-4xl font-bold tabular-nums text-transparent dark:from-white dark:to-slate-400 sm:text-5xl">
{fmt(u.v)}
</div>
<div className="mt-1 text-[0.65rem] font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500">
{u.label}
</div>
</div>
{i < units.length - 1 && (
<span
aria-hidden="true"
className="text-3xl font-bold text-slate-300 dark:text-slate-700"
>
:
</span>
)}
</div>
))}
</div>
) : (
<p className="mt-4 text-2xl font-semibold text-indigo-600 dark:text-indigo-400">
Finalizing — almost there…
</p>
)}
</div>
{/* Progress */}
<div className="mt-8">
<div className="mb-2 flex items-center justify-between text-sm">
<span className="font-medium text-slate-700 dark:text-slate-300">
Overall progress
</span>
<span className="font-semibold tabular-nums text-indigo-600 dark:text-indigo-400">
{progress}%
</span>
</div>
<div
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
aria-valuetext={`${progress} percent complete`}
aria-label="Maintenance completion"
className="h-3 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
>
<div
className="relative h-full overflow-hidden rounded-full bg-gradient-to-r from-indigo-500 to-violet-500"
style={{
width: `${progress}%`,
transition: reduce ? "none" : "width 0.8s ease-out",
}}
>
<span className="em-shimmer absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/50 to-transparent" />
</div>
</div>
</div>
{/* Tasks */}
<ul className="mt-8 space-y-2.5">
{TASKS.map((task) => {
const meta = STATUS_META[task.status];
return (
<li
key={task.id}
className="flex items-center gap-3 rounded-xl border border-slate-200 bg-white px-3.5 py-3 dark:border-slate-800 dark:bg-slate-950/40"
>
<span className="shrink-0">
<StatusIcon status={task.status} />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-slate-800 dark:text-slate-100">
{task.label}
</span>
<span className="block truncate text-xs text-slate-500 dark:text-slate-400">
{task.detail}
</span>
</span>
<span
className={`shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold ring-1 ring-inset ${meta.ring} ${meta.text}`}
>
{meta.word}
</span>
</li>
);
})}
</ul>
</motion.div>
{/* Notify form */}
<motion.div
{...appear(0.36)}
className="mx-auto mt-8 max-w-lg text-center"
>
<h2 className="text-base font-semibold text-slate-800 dark:text-slate-200">
Get notified when we’re back
</h2>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
We’ll send a single email the moment service is restored.
</p>
<form onSubmit={onSubmit} noValidate className="mt-4">
<div className="flex flex-col gap-2 sm:flex-row">
<label htmlFor={emailFieldId} className="sr-only">
Email address
</label>
<input
id={emailFieldId}
name="email"
type="email"
inputMode="email"
autoComplete="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (emailState !== "idle") setEmailState("idle");
}}
placeholder="you@company.com"
aria-invalid={emailState === "error"}
aria-describedby={
emailState === "error"
? emailErrId
: emailState === "success"
? emailOkId
: undefined
}
className={`w-full flex-1 rounded-xl border bg-white px-4 py-2.5 text-sm text-slate-900 placeholder:text-slate-400 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 ${focusRing} ${
emailState === "error"
? "border-rose-400 dark:border-rose-500/60"
: "border-slate-300 dark:border-slate-700"
}`}
/>
<button
type="submit"
className={`inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 active:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-400 ${focusRing}`}
>
Notify me
</button>
</div>
{emailState === "error" && (
<p
id={emailErrId}
role="alert"
className="mt-2 text-left text-sm text-rose-600 dark:text-rose-400"
>
Please enter a valid email address.
</p>
)}
<p
id={emailOkId}
aria-live="polite"
className="mt-2 text-left text-sm text-emerald-600 dark:text-emerald-400"
>
{emailState === "success"
? "You're on the list — we'll email you the moment we're back."
: ""}
</p>
</form>
</motion.div>
{/* Footer actions */}
<motion.div
{...appear(0.44)}
className="mt-10 flex flex-col items-center justify-center gap-4 border-t border-slate-200 pt-8 dark:border-slate-800 sm:flex-row sm:justify-between"
>
<div className="flex items-center gap-3">
<button
type="button"
onClick={onRefresh}
aria-label="Check maintenance status again"
className={`inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-3.5 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 ${focusRing}`}
>
<svg
viewBox="0 0 24 24"
className={`h-4 w-4 ${checking ? "em-spin-ring" : ""}`}
aria-hidden="true"
style={{ transformOrigin: "50% 50%" }}
>
<path
d="M20 11a8 8 0 1 0-2.3 5.7M20 5v6h-6"
fill="none"
strokeWidth={1.9}
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-current"
/>
</svg>
{checking ? "Checking…" : "Check again"}
</button>
<span
aria-live="polite"
className="text-xs text-slate-400 dark:text-slate-500"
>
Updated {checkedLabel}
</span>
</div>
<div className="flex items-center gap-4 text-sm">
<a
href="#status"
className={`rounded-md font-medium text-indigo-600 underline-offset-4 hover:underline dark:text-indigo-400 ${focusRing}`}
>
Live status page
</a>
<a
href="mailto:support@getatlas.io"
className={`rounded-md font-medium text-slate-600 underline-offset-4 hover:underline dark:text-slate-300 ${focusRing}`}
>
Contact support
</a>
</div>
</motion.div>
</div>
<style>{`
@keyframes em-gear-a-kf { to { transform: rotate(360deg); } }
@keyframes em-gear-b-kf { to { transform: rotate(-360deg); } }
@keyframes em-spin-kf { to { transform: rotate(360deg); } }
@keyframes em-pulse-dot { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.45; transform: scale(0.82); } }
@keyframes em-ping-kf { 0% { transform: scale(1); opacity: 0.55; } 100% { transform: scale(2.4); opacity: 0; } }
@keyframes em-shimmer-kf { 0% { transform: translateX(-120%); } 100% { transform: translateX(420%); } }
@keyframes em-float-kf { 0%, 100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, -14px); } }
.em-gear-a { animation: em-gear-a-kf 14s linear infinite; }
.em-gear-b { animation: em-gear-b-kf 9s linear infinite; }
.em-spin-ring { animation: em-spin-kf 1s linear infinite; }
.em-dot { animation: em-pulse-dot 1.8s ease-in-out infinite; }
.em-ping { animation: em-ping-kf 1.8s cubic-bezier(0, 0, 0.2, 1) infinite; }
.em-shimmer { animation: em-shimmer-kf 2.4s ease-in-out infinite; }
.em-float { animation: em-float-kf 7s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.em-gear-a, .em-gear-b, .em-spin-ring, .em-dot, .em-ping, .em-shimmer, .em-float {
animation: none !important;
}
}
`}</style>
</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 Coming Soon
Originalcoming soon page with countdown and email

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.

