Web InnoventixFreeCode

Empty Error

Original · free

something went wrong empty state

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

import { useCallback, useEffect, useId, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type Phase = "failed" | "retrying" | "recovered";

type CheckState = "ok" | "failed" | "checking";

type Check = {
  id: string;
  label: string;
  okDetail: string;
  failDetail: string;
  alwaysOk: boolean;
};

const CHECKS: Check[] = [
  {
    id: "network",
    label: "Your connection",
    okDetail: "Reachable — 38 ms round trip to the edge in Mumbai.",
    failDetail: "Reachable — 38 ms round trip to the edge in Mumbai.",
    alwaysOk: true,
  },
  {
    id: "auth",
    label: "Session & permissions",
    okDetail: "Signed in as salman@webinnoventix.com. Token valid for 6 more days.",
    failDetail: "Signed in as salman@webinnoventix.com. Token valid for 6 more days.",
    alwaysOk: true,
  },
  {
    id: "api",
    label: "Metrics API (us-east-1)",
    okDetail: "Responding in 210 ms. Aggregation pipeline caught up at 14:04 UTC.",
    failDetail: "Returned 503 after 30 s. The warehouse read timed out mid-aggregation.",
    alwaysOk: false,
  },
];

const MAX_ATTEMPTS = 3;
const COOLDOWNS = [6, 10];

const DIAGNOSTIC_REPORT = [
  "Error:      503 Service Unavailable",
  "Endpoint:   GET /v2/metrics/summary?range=30d",
  "Request ID: req_8f3c21ad9b40e7",
  "Region:     us-east-1 (replica 3 of 4)",
  "Time:       2026-07-17 14:02:19 UTC",
  "Trace:      pipeline.aggregate -> warehouse.read (timed out after 30000 ms)",
].join("\n");

function IconAlert({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path
        d="M12 4.2 21 19.5H3L12 4.2Z"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinejoin="round"
      />
      <path d="M12 10v4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
      <circle cx="12" cy="17" r="1.05" fill="currentColor" />
    </svg>
  );
}

function IconCheck({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path
        d="M5 12.5 10 17.5 19 7"
        stroke="currentColor"
        strokeWidth="2.4"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function IconCross({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path d="M6.5 6.5 17.5 17.5M17.5 6.5 6.5 17.5" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" />
    </svg>
  );
}

function IconSpinner({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <circle cx="12" cy="12" r="8" stroke="currentColor" strokeOpacity="0.25" strokeWidth="2.4" />
      <path d="M20 12a8 8 0 0 0-8-8" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" />
    </svg>
  );
}

function IconRefresh({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path
        d="M20 12a8 8 0 1 1-2.6-5.9"
        stroke="currentColor"
        strokeWidth="1.9"
        strokeLinecap="round"
      />
      <path d="M20 4v4.4h-4.4" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function IconChevron({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path d="M7 10l5 5 5-5" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function IconCopy({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <rect x="9" y="9" width="11" height="11" rx="2.4" stroke="currentColor" strokeWidth="1.7" />
      <path
        d="M15 6.2V5.6A1.6 1.6 0 0 0 13.4 4H5.6A1.6 1.6 0 0 0 4 5.6v7.8A1.6 1.6 0 0 0 5.6 15h.6"
        stroke="currentColor"
        strokeWidth="1.7"
        strokeLinecap="round"
      />
    </svg>
  );
}

function StatusDot({ state }: { state: CheckState }) {
  if (state === "checking") {
    return (
      <span className="grid h-6 w-6 shrink-0 place-items-center rounded-full bg-amber-100 text-amber-700 dark:bg-amber-400/15 dark:text-amber-300">
        <IconSpinner className="eerr-spin h-3.5 w-3.5" />
      </span>
    );
  }
  if (state === "failed") {
    return (
      <span className="grid h-6 w-6 shrink-0 place-items-center rounded-full bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300">
        <IconCross className="h-3.5 w-3.5" />
      </span>
    );
  }
  return (
    <span className="grid h-6 w-6 shrink-0 place-items-center rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300">
      <IconCheck className="h-3.5 w-3.5" />
    </span>
  );
}

export default function EmptyError() {
  const reduced = useReducedMotion();
  const headingId = useId();
  const detailsId = useId();
  const statusId = useId();

  const [phase, setPhase] = useState<Phase>("failed");
  const [attempts, setAttempts] = useState(0);
  const [cooldown, setCooldown] = useState(0);
  const [detailsOpen, setDetailsOpen] = useState(false);
  const [copied, setCopied] = useState(false);

  const retryTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const reportRef = useRef<HTMLPreElement | null>(null);

  useEffect(() => {
    return () => {
      if (retryTimer.current) clearTimeout(retryTimer.current);
      if (copyTimer.current) clearTimeout(copyTimer.current);
    };
  }, []);

  useEffect(() => {
    if (cooldown <= 0) return;
    const id = setTimeout(() => setCooldown((value) => Math.max(0, value - 1)), 1000);
    return () => clearTimeout(id);
  }, [cooldown]);

  const runRetry = useCallback(() => {
    if (phase === "retrying" || cooldown > 0) return;
    setPhase("retrying");
    retryTimer.current = setTimeout(() => {
      setAttempts((previous) => {
        const next = previous + 1;
        if (next >= MAX_ATTEMPTS) {
          setPhase("recovered");
        } else {
          setPhase("failed");
          setCooldown(COOLDOWNS[Math.min(next - 1, COOLDOWNS.length - 1)]);
        }
        return next;
      });
    }, 1500);
  }, [cooldown, phase]);

  const resetDemo = useCallback(() => {
    if (retryTimer.current) clearTimeout(retryTimer.current);
    setPhase("failed");
    setAttempts(0);
    setCooldown(0);
  }, []);

  const copyReport = useCallback(async () => {
    let done = false;
    try {
      if (typeof navigator !== "undefined" && navigator.clipboard) {
        await navigator.clipboard.writeText(DIAGNOSTIC_REPORT);
        done = true;
      }
    } catch {
      done = false;
    }
    if (!done && reportRef.current && typeof window !== "undefined") {
      const selection = window.getSelection();
      const range = document.createRange();
      range.selectNodeContents(reportRef.current);
      selection?.removeAllRanges();
      selection?.addRange(range);
    }
    setCopied(true);
    if (copyTimer.current) clearTimeout(copyTimer.current);
    copyTimer.current = setTimeout(() => setCopied(false), 2200);
  }, []);

  const apiState: CheckState =
    phase === "retrying" ? "checking" : phase === "recovered" ? "ok" : "failed";

  const retryDisabled = phase === "retrying" || cooldown > 0;

  const statusMessage =
    phase === "retrying"
      ? `Retrying request, attempt ${attempts + 1} of ${MAX_ATTEMPTS}.`
      : phase === "recovered"
        ? "The metrics API responded. Your dashboard is ready to load."
        : attempts === 0
          ? "The metrics request failed with a 503 error."
          : `Attempt ${attempts} failed. Waiting ${cooldown} seconds before the next try.`;

  return (
    <section
      aria-labelledby={headingId}
      className="relative w-full overflow-hidden bg-zinc-50 px-5 py-20 sm:px-8 sm:py-28 dark:bg-zinc-950"
    >
      <style>{`
        @keyframes eerr-jitter {
          0%, 88%, 100% { transform: translate3d(0, 0, 0); }
          90% { transform: translate3d(-2px, 1px, 0); }
          93% { transform: translate3d(2px, -1px, 0); }
          96% { transform: translate3d(-1px, 0, 0); }
        }
        @keyframes eerr-scan {
          0% { transform: translateY(-130%); opacity: 0; }
          45% { opacity: 1; }
          100% { transform: translateY(130%); opacity: 0; }
        }
        @keyframes eerr-halo {
          0% { transform: scale(0.9); opacity: 0.5; }
          100% { transform: scale(1.55); opacity: 0; }
        }
        @keyframes eerr-spin {
          to { transform: rotate(360deg); }
        }
        .eerr-jitter { animation: eerr-jitter 5s steps(1, end) infinite; }
        .eerr-scan { animation: eerr-scan 3.2s cubic-bezier(0.45, 0, 0.2, 1) infinite; }
        .eerr-halo { animation: eerr-halo 2.6s ease-out infinite; }
        .eerr-spin { animation: eerr-spin 0.9s linear infinite; transform-origin: 50% 50%; }
        @media (prefers-reduced-motion: reduce) {
          .eerr-jitter, .eerr-scan, .eerr-halo, .eerr-spin {
            animation: none !important;
          }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-x-0 top-0 h-80 bg-[radial-gradient(65%_100%_at_50%_0%,rgba(244,63,94,0.13),transparent_72%)] dark:bg-[radial-gradient(65%_100%_at_50%_0%,rgba(244,63,94,0.18),transparent_72%)]"
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 opacity-[0.35] [background-image:linear-gradient(to_right,rgba(100,116,139,0.12)_1px,transparent_1px),linear-gradient(to_bottom,rgba(100,116,139,0.12)_1px,transparent_1px)] [background-size:56px_56px] [mask-image:radial-gradient(70%_60%_at_50%_20%,black,transparent)] dark:opacity-[0.25]"
      />

      <div className="relative mx-auto w-full max-w-2xl">
        <div className="rounded-3xl border border-zinc-200/80 bg-white/85 p-6 shadow-[0_1px_2px_rgba(9,9,11,0.05),0_28px_70px_-32px_rgba(9,9,11,0.3)] backdrop-blur-sm sm:p-10 dark:border-zinc-800 dark:bg-zinc-900/75 dark:shadow-[0_28px_70px_-32px_rgba(0,0,0,0.9)]">
          <div className="flex flex-col items-center text-center">
            <div className="relative mb-7 grid h-24 w-24 place-items-center">
              {phase !== "recovered" ? (
                <span
                  aria-hidden="true"
                  className="eerr-halo absolute inset-2 rounded-2xl border border-rose-400/50 dark:border-rose-400/40"
                />
              ) : null}
              <motion.div
                key={phase === "recovered" ? "ok" : "bad"}
                initial={reduced ? false : { scale: 0.82, opacity: 0 }}
                animate={{ scale: 1, opacity: 1 }}
                transition={{ type: "spring", stiffness: 280, damping: 19 }}
                className={`relative grid h-20 w-20 place-items-center overflow-hidden rounded-2xl text-white shadow-lg ${
                  phase === "recovered"
                    ? "bg-emerald-500 shadow-emerald-500/25 dark:bg-emerald-400 dark:text-emerald-950 dark:shadow-emerald-400/20"
                    : "eerr-jitter bg-rose-500 shadow-rose-500/25 dark:bg-rose-500 dark:shadow-rose-500/25"
                }`}
              >
                {phase === "recovered" ? (
                  <IconCheck className="h-9 w-9" />
                ) : (
                  <IconAlert className="h-9 w-9" />
                )}
                <span
                  aria-hidden="true"
                  className="eerr-scan absolute inset-x-0 -top-1/3 h-1/3 bg-white/25"
                />
              </motion.div>
            </div>

            <p className="mb-3 inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-zinc-100/80 px-3 py-1 font-mono text-[11px] font-medium tracking-wide text-zinc-600 dark:border-zinc-800 dark:bg-zinc-950/60 dark:text-zinc-400">
              <span
                aria-hidden="true"
                className={`h-1.5 w-1.5 rounded-full ${
                  phase === "recovered" ? "bg-emerald-500" : "bg-rose-500"
                }`}
              />
              503 · req_8f3c21ad9b40e7
            </p>

            <h2
              id={headingId}
              className="text-balance text-3xl font-semibold tracking-tight text-zinc-900 sm:text-4xl dark:text-zinc-50"
            >
              {phase === "recovered"
                ? "Back online. Nothing was lost."
                : "We couldn't load your metrics."}
            </h2>
            <p className="mt-3 max-w-md text-pretty text-base leading-relaxed text-zinc-600 dark:text-zinc-400">
              {phase === "recovered"
                ? "The warehouse replica finished catching up and returned all 30 days of data. Your filters and date range were kept exactly as you left them."
                : "The request timed out while our warehouse replica was catching up. This is on us, not on you — your data is safe and nothing was deleted."}
            </p>

            <p
              id={statusId}
              role="status"
              aria-live="polite"
              className="mt-3 min-h-5 text-sm font-medium text-zinc-500 dark:text-zinc-400"
            >
              {statusMessage}
            </p>
          </div>

          <div className="mt-8 flex flex-col gap-3 sm:flex-row sm:justify-center">
            {phase === "recovered" ? (
              <>
                <a
                  href="#dashboard"
                  className="inline-flex items-center justify-center gap-2 rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-emerald-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-emerald-500 dark:hover:bg-emerald-400 dark:focus-visible:ring-emerald-400 dark:focus-visible:ring-offset-zinc-900"
                >
                  Open the dashboard
                </a>
                <button
                  type="button"
                  onClick={resetDemo}
                  className="inline-flex items-center justify-center gap-2 rounded-xl border border-zinc-300 px-5 py-2.5 text-sm font-semibold text-zinc-700 transition-colors hover:bg-zinc-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-rose-400 dark:focus-visible:ring-offset-zinc-900"
                >
                  Replay the failure
                </button>
              </>
            ) : (
              <>
                <button
                  type="button"
                  onClick={runRetry}
                  disabled={retryDisabled}
                  aria-describedby={statusId}
                  className="inline-flex items-center justify-center gap-2 rounded-xl bg-zinc-900 px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-55 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-white dark:focus-visible:ring-rose-400 dark:focus-visible:ring-offset-zinc-900"
                >
                  {phase === "retrying" ? (
                    <IconSpinner className="eerr-spin h-4 w-4" />
                  ) : (
                    <IconRefresh className="h-4 w-4" />
                  )}
                  {phase === "retrying"
                    ? "Retrying…"
                    : cooldown > 0
                      ? `Retry in ${cooldown}s`
                      : attempts === 0
                        ? "Try again"
                        : `Try again (attempt ${attempts + 1})`}
                </button>
                <a
                  href="#status"
                  className="inline-flex items-center justify-center gap-2 rounded-xl border border-zinc-300 px-5 py-2.5 text-sm font-semibold text-zinc-700 transition-colors hover:bg-zinc-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-rose-400 dark:focus-visible:ring-offset-zinc-900"
                >
                  Check the status page
                </a>
              </>
            )}
          </div>

          <ul className="mt-9 space-y-2.5">
            {CHECKS.map((check) => {
              const state: CheckState = check.alwaysOk ? "ok" : apiState;
              return (
                <li
                  key={check.id}
                  className="flex items-start gap-3 rounded-2xl border border-zinc-200 bg-white px-4 py-3.5 text-left dark:border-zinc-800 dark:bg-zinc-900"
                >
                  <span className="mt-0.5">
                    <StatusDot state={state} />
                  </span>
                  <span className="min-w-0 flex-1">
                    <span className="block text-sm font-medium text-zinc-900 dark:text-zinc-100">
                      {check.label}
                    </span>
                    <motion.span
                      key={`${check.id}-${state}`}
                      initial={reduced ? false : { opacity: 0, y: 4 }}
                      animate={{ opacity: 1, y: 0 }}
                      transition={{ duration: 0.24, ease: "easeOut" }}
                      className="mt-0.5 block text-pretty text-sm leading-relaxed text-zinc-500 dark:text-zinc-400"
                    >
                      {state === "checking"
                        ? "Re-running the request against replica 3…"
                        : state === "failed"
                          ? check.failDetail
                          : check.okDetail}
                    </motion.span>
                  </span>
                </li>
              );
            })}
          </ul>

          <div className="mt-6 overflow-hidden rounded-2xl border border-zinc-200 dark:border-zinc-800">
            <h3>
              <button
                type="button"
                onClick={() => setDetailsOpen((open) => !open)}
                aria-expanded={detailsOpen}
                aria-controls={detailsId}
                className="flex w-full items-center justify-between gap-3 bg-zinc-50 px-4 py-3 text-left text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-rose-500 dark:bg-zinc-950/50 dark:text-zinc-300 dark:hover:bg-zinc-950 dark:focus-visible:ring-rose-400"
              >
                <span>Technical details for support</span>
                <IconChevron
                  className={`h-4 w-4 shrink-0 transition-transform duration-200 ${
                    detailsOpen ? "rotate-180" : ""
                  }`}
                />
              </button>
            </h3>
            <div id={detailsId} hidden={!detailsOpen} className="bg-white dark:bg-zinc-900">
              <div className="space-y-3 px-4 py-4">
                <pre
                  ref={reportRef}
                  className="overflow-x-auto rounded-xl bg-zinc-900 p-4 font-mono text-[12px] leading-relaxed text-zinc-200 dark:bg-zinc-950 dark:text-zinc-300"
                >
                  {DIAGNOSTIC_REPORT}
                </pre>
                <div className="flex flex-wrap items-center gap-3">
                  <button
                    type="button"
                    onClick={copyReport}
                    className="inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-3 py-1.5 text-xs font-semibold text-zinc-700 transition-colors hover:bg-zinc-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800 dark:focus-visible:ring-rose-400 dark:focus-visible:ring-offset-zinc-900"
                  >
                    {copied ? <IconCheck className="h-3.5 w-3.5" /> : <IconCopy className="h-3.5 w-3.5" />}
                    {copied ? "Copied to clipboard" : "Copy report"}
                  </button>
                  <span role="status" aria-live="polite" className="sr-only">
                    {copied ? "Diagnostic report copied to clipboard." : ""}
                  </span>
                  <a
                    href="#support"
                    className="rounded-md text-xs font-medium text-rose-600 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-rose-400 dark:focus-visible:ring-rose-400 dark:focus-visible:ring-offset-zinc-900"
                  >
                    Send this to a human
                  </a>
                </div>
              </div>
            </div>
          </div>
        </div>

        <p className="mt-5 text-center text-xs text-zinc-500 dark:text-zinc-500">
          Incident INC-2291 opened automatically · 3 of 4 replicas healthy · Updated every 30 seconds
        </p>
      </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 →