Error Generic
Original · freegeneric error page with retry
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-generic.json"use client";
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Status = "error" | "retrying" | "restored";
type LogKind = "info" | "fail" | "success";
interface LogEntry {
id: number;
kind: LogKind;
message: string;
time: string;
}
interface StatusFace {
badge: string;
heading: string;
desc: string;
dot: string;
chip: string;
}
const RECOVER_AT = 3;
const ENDPOINT = "POST api.internal/v2/session/resume";
const STATUS_UI: Record<Status, StatusFace> = {
error: {
badge: "503 · Service unavailable",
heading: "Something went wrong on our end",
desc: "The dashboard couldn't reach our servers. This is almost always temporary and nothing you did caused it. Your work is saved. Give it another try below.",
dot: "bg-rose-500",
chip: "border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-300",
},
retrying: {
badge: "Reconnecting…",
heading: "Trying to reconnect",
desc: "We're re-establishing a secure connection to the service. This usually takes a couple of seconds — hang tight.",
dot: "bg-amber-500",
chip: "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-300",
},
restored: {
badge: "Connection restored",
heading: "You're back online",
desc: "We reconnected successfully and everything checks out. Pick up right where you left off — no data was lost.",
dot: "bg-emerald-500",
chip: "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-300",
},
};
const DOTS: { top: string; left: string; size: number; delay: string; tone: string }[] = [
{ top: "12%", left: "8%", size: 10, delay: "0s", tone: "bg-indigo-400/50 dark:bg-indigo-400/40" },
{ top: "26%", left: "88%", size: 7, delay: "0.9s", tone: "bg-rose-400/50 dark:bg-rose-400/40" },
{ top: "68%", left: "6%", size: 8, delay: "1.7s", tone: "bg-sky-400/50 dark:bg-sky-400/40" },
{ top: "80%", left: "84%", size: 12, delay: "0.4s", tone: "bg-violet-400/50 dark:bg-violet-400/40" },
{ top: "44%", left: "94%", size: 6, delay: "2.3s", tone: "bg-emerald-400/50 dark:bg-emerald-400/40" },
{ top: "88%", left: "40%", size: 6, delay: "1.2s", tone: "bg-amber-400/50 dark:bg-amber-400/40" },
];
function makeTraceId(): string {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789";
let raw = "";
for (let i = 0; i < 12; i += 1) raw += chars[Math.floor(Math.random() * chars.length)];
return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`;
}
export default function ErrorGeneric() {
const reduce = useReducedMotion();
const [status, setStatus] = useState<Status>("error");
const [attempts, setAttempts] = useState<number>(0);
const [log, setLog] = useState<LogEntry[]>([]);
const [detailsOpen, setDetailsOpen] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const [traceId, setTraceId] = useState<string>("————-————-————");
const [occurredAt, setOccurredAt] = useState<string>("—");
const timeoutRef = useRef<number | null>(null);
const copyRef = useRef<number | null>(null);
const logSeq = useRef<number>(0);
useEffect(() => {
setTraceId(makeTraceId());
setOccurredAt(
new Date().toLocaleString([], {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
);
return () => {
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
if (copyRef.current) window.clearTimeout(copyRef.current);
};
}, []);
function pushLog(kind: LogKind, message: string) {
logSeq.current += 1;
const entry: LogEntry = {
id: logSeq.current,
kind,
message,
time: new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
};
setLog((prev) => [entry, ...prev].slice(0, 5));
}
function handleRetry() {
if (status === "retrying") return;
const nextAttempt = attempts + 1;
setStatus("retrying");
pushLog("info", `Retry #${nextAttempt} — ${ENDPOINT}`);
timeoutRef.current = window.setTimeout(() => {
setAttempts(nextAttempt);
if (nextAttempt >= RECOVER_AT) {
setStatus("restored");
pushLog("success", `Attempt ${nextAttempt} succeeded — connection healthy again.`);
} else {
setStatus("error");
pushLog("fail", `Attempt ${nextAttempt} failed — upstream timed out (504).`);
}
}, 1500);
}
function handleReset() {
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
setStatus("error");
setAttempts(0);
setLog([]);
}
async function handleCopy() {
try {
await navigator.clipboard.writeText(traceId);
setCopied(true);
} catch {
setCopied(true);
}
if (copyRef.current) window.clearTimeout(copyRef.current);
copyRef.current = window.setTimeout(() => setCopied(false), 1800);
}
const ui = STATUS_UI[status];
const retrying = status === "retrying";
const restored = status === "restored";
const logTone: Record<LogKind, string> = {
info: "text-slate-500 dark:text-slate-400",
fail: "text-rose-600 dark:text-rose-400",
success: "text-emerald-600 dark:text-emerald-400",
};
const logMarker: Record<LogKind, string> = {
info: "bg-slate-400 dark:bg-slate-500",
fail: "bg-rose-500",
success: "bg-emerald-500",
};
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 px-6 py-20 sm:py-28 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900">
<style>{`
@keyframes errgen-orbit { to { transform: rotate(360deg); } }
@keyframes errgen-spin { to { transform: rotate(360deg); } }
@keyframes errgen-float {
0%, 100% { transform: translateY(0); opacity: 0.55; }
50% { transform: translateY(-14px); opacity: 1; }
}
@keyframes errgen-pulse {
0%, 100% { transform: scale(0.82); opacity: 0.35; }
50% { transform: scale(1.12); opacity: 1; }
}
@keyframes errgen-shimmer {
0% { transform: translateX(-120%); }
100% { transform: translateX(320%); }
}
.errgen-orbit { animation: errgen-orbit 22s linear infinite; transform-origin: center; }
.errgen-spin { animation: errgen-spin 0.75s linear infinite; transform-origin: center; }
.errgen-float { animation: errgen-float 6s ease-in-out infinite; }
.errgen-pulse { animation: errgen-pulse 2.2s ease-in-out infinite; transform-origin: center; }
.errgen-shimmer { animation: errgen-shimmer 1.15s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
@media (prefers-reduced-motion: reduce) {
.errgen-orbit, .errgen-spin, .errgen-float, .errgen-pulse, .errgen-shimmer {
animation: none !important;
}
}
`}</style>
{/* Decorative floating particles */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
{DOTS.map((d, i) => (
<span
key={i}
className={`errgen-float absolute rounded-full ${d.tone}`}
style={{ top: d.top, left: d.left, width: d.size, height: d.size, animationDelay: d.delay }}
/>
))}
<div className="absolute left-1/2 top-0 h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-300/20 blur-3xl dark:bg-indigo-600/10" />
</div>
<motion.div
initial={reduce ? false : { opacity: 0, y: 18 }}
animate={reduce ? undefined : { opacity: 1, y: 0 }}
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
className="relative mx-auto w-full max-w-2xl overflow-hidden rounded-3xl border border-slate-200/80 bg-white/85 p-7 shadow-[0_24px_60px_-24px_rgba(15,23,42,0.35)] backdrop-blur-sm sm:p-10 dark:border-slate-800 dark:bg-slate-900/75 dark:shadow-[0_24px_70px_-28px_rgba(0,0,0,0.75)]"
>
{/* Status badge */}
<div className="flex items-center justify-between gap-4">
<span
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold tracking-wide ${ui.chip}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${ui.dot} ${retrying ? "errgen-pulse" : ""}`} />
{ui.badge}
</span>
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-mono text-[11px] tabular-nums text-slate-500 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400">
{attempts} {attempts === 1 ? "attempt" : "attempts"}
</span>
</div>
{/* Illustration: unplugged connection */}
<div className="mt-8 flex justify-center">
<svg
viewBox="0 0 260 180"
role="img"
aria-label="Illustration of a disconnected cable"
className="h-40 w-full max-w-sm"
>
<circle
cx="130"
cy="90"
r="72"
fill="none"
strokeWidth="2"
strokeDasharray="4 10"
strokeLinecap="round"
className="errgen-orbit stroke-slate-300 dark:stroke-slate-700"
style={{ transformBox: "fill-box", transformOrigin: "center" }}
/>
<circle cx="130" cy="90" r="50" className="fill-slate-100/70 dark:fill-slate-800/50" />
{/* left cable + plug */}
<path
d="M14 150 C 54 150 70 90 104 90"
fill="none"
strokeWidth="7"
strokeLinecap="round"
className="stroke-slate-400 dark:stroke-slate-500"
/>
<rect x="102" y="76" width="22" height="28" rx="6" className="fill-indigo-500" />
<rect x="122" y="82" width="10" height="4" rx="2" className="fill-indigo-400" />
<rect x="122" y="94" width="10" height="4" rx="2" className="fill-indigo-400" />
{/* right cable + socket */}
<path
d="M246 30 C 206 30 190 90 156 90"
fill="none"
strokeWidth="7"
strokeLinecap="round"
className="stroke-slate-400 dark:stroke-slate-500"
/>
<rect x="150" y="72" width="24" height="36" rx="7" className="fill-slate-500 dark:fill-slate-600" />
<circle cx="158" cy="84" r="3" className="fill-slate-200 dark:fill-slate-900" />
<circle cx="158" cy="96" r="3" className="fill-slate-200 dark:fill-slate-900" />
{/* spark gap */}
<g className={retrying ? "" : "errgen-pulse"} style={{ transformBox: "fill-box", transformOrigin: "138px 90px" }}>
<path
d="M136 78 L131 90 L139 90 L134 102"
fill="none"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
className={restored ? "stroke-emerald-500" : "stroke-rose-500"}
/>
<circle cx="143" cy="74" r="2" className={restored ? "fill-emerald-400" : "fill-rose-400"} />
<circle cx="127" cy="106" r="2" className={restored ? "fill-emerald-400" : "fill-amber-400"} />
</g>
</svg>
</div>
{/* Copy */}
<div className="mt-6 text-center">
<h1 className="text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-50">
{ui.heading}
</h1>
<p className="mx-auto mt-3 max-w-md text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{ui.desc}
</p>
</div>
{/* Actions */}
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:justify-center">
{restored ? (
<>
<a
href="#dashboard"
className="group relative inline-flex items-center justify-center gap-2 rounded-xl bg-emerald-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-emerald-600/25 transition hover:bg-emerald-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
>
Continue to dashboard
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="none" aria-hidden="true">
<path d="M4 10h11m0 0-4-4m4 4-4 4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</a>
<button
type="button"
onClick={handleReset}
className="inline-flex items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Run the demo again
</button>
</>
) : (
<>
<button
type="button"
onClick={handleRetry}
disabled={retrying}
aria-busy={retrying}
className="group relative inline-flex min-w-[9.5rem] items-center justify-center gap-2 overflow-hidden rounded-xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-wait disabled:opacity-90 dark:focus-visible:ring-offset-slate-900"
>
{retrying ? (
<>
<svg viewBox="0 0 20 20" className="errgen-spin h-4 w-4" fill="none" aria-hidden="true">
<path d="M10 3a7 7 0 1 0 7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
Reconnecting…
<span className="pointer-events-none absolute inset-y-0 -left-1/3 w-1/3 skew-x-[-20deg] bg-white/25 errgen-shimmer" aria-hidden="true" />
</>
) : (
<>
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="none" aria-hidden="true">
<path d="M15.5 8A6 6 0 1 0 16 11" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M16 4v4h-4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Try again
</>
)}
</button>
<a
href="#home"
className="inline-flex items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Go to homepage
</a>
</>
)}
</div>
{/* Retry activity log — announced to assistive tech */}
<div
role="status"
aria-live="polite"
className="mt-8 rounded-2xl border border-slate-200 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-950/40"
>
<div className="flex items-center justify-between">
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Connection log
</span>
<span className="font-mono text-[11px] text-slate-400 dark:text-slate-500">retry ≤ {RECOVER_AT}</span>
</div>
{log.length === 0 ? (
<p className="mt-3 text-sm text-slate-400 dark:text-slate-500">
No attempts yet. Press <span className="font-medium text-slate-500 dark:text-slate-400">Try again</span> to reconnect.
</p>
) : (
<ul className="mt-3 space-y-2">
<AnimatePresence initial={false}>
{log.map((entry) => (
<motion.li
key={entry.id}
initial={reduce ? false : { opacity: 0, x: -8 }}
animate={reduce ? undefined : { opacity: 1, x: 0 }}
exit={reduce ? undefined : { opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="flex items-start gap-2.5 text-sm"
>
<span className={`mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full ${logMarker[entry.kind]}`} />
<span className={`flex-1 ${logTone[entry.kind]}`}>{entry.message}</span>
<span className="shrink-0 font-mono text-[11px] tabular-nums text-slate-400 dark:text-slate-600">
{entry.time}
</span>
</motion.li>
))}
</AnimatePresence>
</ul>
)}
</div>
{/* Technical details disclosure */}
<div className="mt-4">
<button
type="button"
onClick={() => setDetailsOpen((o) => !o)}
aria-expanded={detailsOpen}
aria-controls="errgen-details"
className="inline-flex items-center gap-1.5 rounded-lg px-1 text-sm font-medium text-slate-500 transition hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-900"
>
<svg
viewBox="0 0 20 20"
className={`h-4 w-4 transition-transform duration-200 ${detailsOpen ? "rotate-90" : ""}`}
fill="none"
aria-hidden="true"
>
<path d="M7 5l6 5-6 5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{detailsOpen ? "Hide technical details" : "Show technical details"}
</button>
<AnimatePresence initial={false}>
{detailsOpen && (
<motion.div
id="errgen-details"
initial={reduce ? false : { height: 0, opacity: 0 }}
animate={reduce ? undefined : { height: "auto", opacity: 1 }}
exit={reduce ? undefined : { height: 0, opacity: 0 }}
transition={{ duration: 0.28, ease: "easeInOut" }}
className="overflow-hidden"
>
<dl className="mt-3 grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-slate-200 bg-slate-200 text-sm sm:grid-cols-2 dark:border-slate-800 dark:bg-slate-800">
<DetailRow label="Status code" value="503 · UPSTREAM_TIMEOUT" mono />
<DetailRow label="Occurred at" value={occurredAt} />
<DetailRow label="Endpoint" value="/v2/session/resume" mono />
<DetailRow label="Region" value="iad-1 · edge" />
<div className="flex items-center justify-between gap-3 bg-white px-4 py-3 sm:col-span-2 dark:bg-slate-900">
<div className="min-w-0">
<dt className="text-xs font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
Request ID
</dt>
<dd className="truncate font-mono text-slate-700 dark:text-slate-200">{traceId}</dd>
</div>
<button
type="button"
onClick={handleCopy}
className="inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-semibold text-slate-600 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
>
{copied ? (
<>
<svg viewBox="0 0 20 20" className="h-3.5 w-3.5 text-emerald-500" fill="none" aria-hidden="true">
<path d="M4 10.5l4 4 8-9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Copied
</>
) : (
<>
<svg viewBox="0 0 20 20" className="h-3.5 w-3.5" fill="none" aria-hidden="true">
<rect x="7" y="7" width="9" height="9" rx="2" stroke="currentColor" strokeWidth="1.6" />
<path d="M13 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
</svg>
Copy ID
</>
)}
</button>
</div>
</dl>
<p className="mt-3 text-xs leading-relaxed text-slate-400 dark:text-slate-500">
Include the request ID above if you contact support. It lets us trace this exact
session across our logs.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
<p className="relative mx-auto mt-6 max-w-2xl text-center text-xs text-slate-400 dark:text-slate-500">
Still stuck after a few tries? Check{" "}
<a
href="#status"
className="font-medium text-slate-500 underline decoration-slate-300 underline-offset-2 transition hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:decoration-slate-600 dark:hover:text-slate-200 dark:focus-visible:ring-offset-slate-950"
>
our status page
</a>{" "}
or reach support.
</p>
</section>
);
}
function DetailRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div className="bg-white px-4 py-3 dark:bg-slate-900">
<dt className="text-xs font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">{label}</dt>
<dd className={`mt-0.5 text-slate-700 dark:text-slate-200 ${mono ? "font-mono text-[13px]" : ""}`}>{value}</dd>
</div>
);
}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 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

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.

