Web InnoventixFreeCode

Error Generic

Original · free

generic error page with retry

byWeb InnoventixReact + Tailwind
errorgenericpages
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-generic.json
error-generic.tsx
"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

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 →