Web InnoventixFreeCode

Error 404

Original · free

friendly 404 page with illustration and CTA

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

import {
  useEffect,
  useId,
  useRef,
  useState,
  type KeyboardEvent as ReactKeyboardEvent,
  type PointerEvent as ReactPointerEvent,
} from "react";
import {
  motion,
  useMotionValue,
  useReducedMotion,
  useSpring,
  useTransform,
} from "motion/react";

interface Destination {
  label: string;
  href: string;
  desc: string;
}

const DESTINATIONS: Destination[] = [
  { label: "Homepage", href: "/", desc: "Start fresh from the front door" },
  { label: "Pricing & plans", href: "/pricing", desc: "Compare tiers, features and limits" },
  { label: "Documentation", href: "/docs", desc: "Guides, API reference and tutorials" },
  { label: "Blog", href: "/blog", desc: "Latest articles, notes and updates" },
  { label: "Contact support", href: "/contact", desc: "Talk to a real human on the team" },
  { label: "System status", href: "/status", desc: "Live uptime and incident history" },
  { label: "Careers", href: "/careers", desc: "Open roles across every team" },
  { label: "Changelog", href: "/changelog", desc: "Everything we shipped recently" },
];

const REFERENCE_CODE = "REQ-404-9F2C7A1B";

const STARS: { cx: number; cy: number; r: number; delay: number }[] = [
  { cx: 54, cy: 66, r: 1.6, delay: 0 },
  { cx: 120, cy: 40, r: 2.4, delay: 0.6 },
  { cx: 300, cy: 54, r: 1.8, delay: 1.2 },
  { cx: 366, cy: 96, r: 2.6, delay: 0.3 },
  { cx: 388, cy: 210, r: 1.5, delay: 0.9 },
  { cx: 40, cy: 176, r: 2.2, delay: 1.5 },
  { cx: 350, cy: 300, r: 2, delay: 0.2 },
  { cx: 66, cy: 300, r: 1.7, delay: 1.1 },
  { cx: 250, cy: 24, r: 1.4, delay: 1.8 },
  { cx: 180, cy: 92, r: 1.5, delay: 0.75 },
  { cx: 330, cy: 160, r: 1.9, delay: 1.35 },
  { cx: 24, cy: 250, r: 1.6, delay: 0.5 },
];

export default function Error404Page() {
  const prefersReduced = useReducedMotion();
  const reduce = Boolean(prefersReduced);

  const uid = useId();
  const inputId = `${uid}-input`;
  const listId = `${uid}-list`;
  const titleId = `${uid}-title`;
  const descId = `${uid}-desc`;
  const liveId = `${uid}-live`;
  const optionId = (i: number) => `${uid}-opt-${i}`;

  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const [copied, setCopied] = useState(false);

  const rootRef = useRef<HTMLDivElement | null>(null);
  const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

  const q = query.trim().toLowerCase();
  const results =
    q === ""
      ? DESTINATIONS
      : DESTINATIONS.filter((d) =>
          `${d.label} ${d.desc}`.toLowerCase().includes(q),
        );

  // Parallax motion values (pointer-driven, disabled under reduced motion).
  const mvx = useMotionValue(0);
  const mvy = useMotionValue(0);
  const sx = useSpring(mvx, { stiffness: 110, damping: 18, mass: 0.4 });
  const sy = useSpring(mvy, { stiffness: 110, damping: 18, mass: 0.4 });

  const starsX = useTransform(sx, (v) => v * 0.35);
  const starsY = useTransform(sy, (v) => v * 0.35);
  const planetX = useTransform(sx, (v) => v * 0.7);
  const planetY = useTransform(sy, (v) => v * 0.7);
  const astroX = useTransform(sx, (v) => v * 1.15);
  const astroY = useTransform(sy, (v) => v * 1.15);
  const frontX = useTransform(sx, (v) => v * 1.7);
  const frontY = useTransform(sy, (v) => v * 1.7);

  const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
    if (reduce) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const nx = (e.clientX - rect.left) / rect.width - 0.5;
    const ny = (e.clientY - rect.top) / rect.height - 0.5;
    mvx.set(nx * 36);
    mvy.set(ny * 36);
  };
  const onPointerLeave = () => {
    mvx.set(0);
    mvy.set(0);
  };

  // Close the listbox on an outside pointer press.
  useEffect(() => {
    const handler = (e: globalThis.PointerEvent) => {
      if (!rootRef.current) return;
      if (!rootRef.current.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener("pointerdown", handler);
    return () => document.removeEventListener("pointerdown", handler);
  }, []);

  // Keep the active option scrolled into view.
  useEffect(() => {
    if (activeIndex < 0) return;
    const el = document.getElementById(optionId(activeIndex));
    if (el) el.scrollIntoView({ block: "nearest" });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeIndex]);

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

  const go = (href: string) => {
    window.location.assign(href);
  };

  const onInputKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
    if (e.key === "ArrowDown") {
      e.preventDefault();
      if (!open) setOpen(true);
      setActiveIndex((prev) => {
        const max = results.length - 1;
        if (max < 0) return -1;
        return prev < max ? prev + 1 : max;
      });
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      if (!open) setOpen(true);
      setActiveIndex((prev) => (prev > 0 ? prev - 1 : 0));
    } else if (e.key === "Enter") {
      if (open && activeIndex >= 0 && activeIndex < results.length) {
        e.preventDefault();
        go(results[activeIndex].href);
      }
    } else if (e.key === "Escape") {
      setOpen(false);
      setActiveIndex(-1);
    }
  };

  const copyReference = async () => {
    try {
      await navigator.clipboard.writeText(REFERENCE_CODE);
      setCopied(true);
      if (copyTimer.current) clearTimeout(copyTimer.current);
      copyTimer.current = setTimeout(() => setCopied(false), 2200);
    } catch {
      setCopied(false);
    }
  };

  const fadeUp = (delay: number) =>
    reduce
      ? { initial: false as const }
      : {
          initial: { opacity: 0, y: 14 },
          animate: { opacity: 1, y: 0 },
          transition: { duration: 0.55, delay, ease: [0.16, 1, 0.3, 1] as const },
        };

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes err404-twinkle {
          0%, 100% { opacity: 0.25; transform: scale(0.7); }
          50% { opacity: 1; transform: scale(1); }
        }
        @keyframes err404-float {
          0%, 100% { transform: translateY(0px) rotate(-1.5deg); }
          50% { transform: translateY(-14px) rotate(1.5deg); }
        }
        @keyframes err404-drift {
          0%, 100% { transform: translateY(0px); }
          50% { transform: translateY(8px); }
        }
        @keyframes err404-orbit {
          from { transform: rotate(0deg); }
          to { transform: rotate(360deg); }
        }
        @keyframes err404-blob {
          0%, 100% { transform: translate3d(0,0,0) scale(1); }
          50% { transform: translate3d(0,-18px,0) scale(1.08); }
        }
        .err404-twinkle { animation: err404-twinkle 3.2s ease-in-out infinite; }
        .err404-float { animation: err404-float 6.5s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
        .err404-drift { animation: err404-drift 9s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
        .err404-orbit { animation: err404-orbit 16s linear infinite; transform-box: view-box; transform-origin: 210px 210px; }
        .err404-blob { animation: err404-blob 11s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .err404-twinkle, .err404-float, .err404-drift, .err404-orbit, .err404-blob {
            animation: none !important;
          }
        }
      `}</style>

      {/* Atmosphere */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(70%_55%_at_50%_-10%,rgba(99,102,241,0.14),transparent)]"
      />
      <div
        aria-hidden="true"
        className="err404-blob pointer-events-none absolute -left-24 top-24 -z-10 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/10"
      />
      <div
        aria-hidden="true"
        className="err404-blob pointer-events-none absolute -right-16 bottom-0 -z-10 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-500/10"
        style={{ animationDelay: "3s" }}
      />

      <div className="mx-auto flex min-h-screen w-full max-w-6xl items-center px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
        <div className="grid w-full items-center gap-12 lg:grid-cols-2 lg:gap-16">
          {/* Copy + controls */}
          <div className="order-2 lg:order-1">
            <motion.div {...fadeUp(0)}>
              <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
                <svg
                  viewBox="0 0 24 24"
                  className="h-3.5 w-3.5"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  aria-hidden="true"
                >
                  <circle cx="12" cy="12" r="9" />
                  <path d="M14.5 9.5 11 11l-1.5 3.5L13 13z" />
                </svg>
                Error 404
              </span>
            </motion.div>

            <motion.h1
              {...fadeUp(0.06)}
              className="mt-6 text-4xl font-black leading-[1.05] tracking-tight text-slate-900 sm:text-5xl lg:text-6xl dark:text-white"
            >
              This page took a
              <span className="bg-gradient-to-r from-indigo-600 to-violet-600 bg-clip-text text-transparent dark:from-indigo-400 dark:to-violet-400">
                {" "}
                wrong turn.
              </span>
            </motion.h1>

            <motion.p
              {...fadeUp(0.12)}
              className="mt-5 max-w-md text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-400"
            >
              The link may be broken, or the page might have drifted off into
              orbit. Search for what you need, or jump to a popular destination
              below.
            </motion.p>

            {/* Combobox search */}
            <motion.div {...fadeUp(0.18)} className="mt-8">
              <label
                htmlFor={inputId}
                className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-300"
              >
                Search the site
              </label>
              <div ref={rootRef} className="relative">
                <div className="relative">
                  <svg
                    viewBox="0 0 24 24"
                    className="pointer-events-none absolute left-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400 dark:text-slate-500"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    aria-hidden="true"
                  >
                    <circle cx="11" cy="11" r="7" />
                    <path d="m20 20-3-3" />
                  </svg>
                  <input
                    id={inputId}
                    type="text"
                    role="combobox"
                    aria-expanded={open}
                    aria-controls={listId}
                    aria-autocomplete="list"
                    aria-activedescendant={
                      open && activeIndex >= 0 ? optionId(activeIndex) : undefined
                    }
                    autoComplete="off"
                    placeholder="Try 'pricing', 'docs' or 'status'…"
                    value={query}
                    onChange={(e) => {
                      setQuery(e.target.value);
                      setOpen(true);
                      setActiveIndex(-1);
                    }}
                    onFocus={() => setOpen(true)}
                    onKeyDown={onInputKeyDown}
                    className="w-full rounded-xl border border-slate-300 bg-white py-3 pl-11 pr-4 text-sm text-slate-900 shadow-sm outline-none transition placeholder:text-slate-400 focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus-visible:border-indigo-400"
                  />
                </div>

                {open && (
                  <ul
                    id={listId}
                    role="listbox"
                    aria-label="Suggested destinations"
                    className="absolute z-20 mt-2 max-h-72 w-full overflow-auto rounded-xl border border-slate-200 bg-white p-1.5 shadow-xl shadow-slate-900/10 dark:border-slate-700 dark:bg-slate-900 dark:shadow-black/40"
                  >
                    {results.length === 0 ? (
                      <li
                        role="status"
                        className="px-3 py-6 text-center text-sm text-slate-500 dark:text-slate-400"
                      >
                        No matches for “{query}”. Try{" "}
                        <span className="font-medium text-slate-700 dark:text-slate-200">
                          pricing
                        </span>{" "}
                        or{" "}
                        <span className="font-medium text-slate-700 dark:text-slate-200">
                          contact
                        </span>
                        .
                      </li>
                    ) : (
                      results.map((d, i) => {
                        const active = i === activeIndex;
                        return (
                          <li key={d.href} role="none">
                            <a
                              id={optionId(i)}
                              role="option"
                              aria-selected={active}
                              href={d.href}
                              onMouseEnter={() => setActiveIndex(i)}
                              className={`flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 text-left outline-none transition ${
                                active
                                  ? "bg-indigo-50 dark:bg-indigo-500/15"
                                  : "hover:bg-slate-50 dark:hover:bg-slate-800/60"
                              }`}
                            >
                              <span className="min-w-0">
                                <span className="block truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
                                  {d.label}
                                </span>
                                <span className="block truncate text-xs text-slate-500 dark:text-slate-400">
                                  {d.desc}
                                </span>
                              </span>
                              <svg
                                viewBox="0 0 24 24"
                                className={`h-4 w-4 shrink-0 transition ${
                                  active
                                    ? "text-indigo-600 dark:text-indigo-400"
                                    : "text-slate-300 dark:text-slate-600"
                                }`}
                                fill="none"
                                stroke="currentColor"
                                strokeWidth="2"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                                aria-hidden="true"
                              >
                                <path d="M5 12h14" />
                                <path d="m13 6 6 6-6 6" />
                              </svg>
                            </a>
                          </li>
                        );
                      })
                    )}
                  </ul>
                )}
              </div>
            </motion.div>

            {/* CTAs */}
            <motion.div
              {...fadeUp(0.24)}
              className="mt-8 flex flex-wrap items-center gap-3"
            >
              <a
                href="/"
                className="inline-flex items-center gap-2 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-slate-50 dark:focus-visible:ring-offset-slate-950"
              >
                <svg
                  viewBox="0 0 24 24"
                  className="h-4 w-4"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  aria-hidden="true"
                >
                  <path d="m3 10.5 9-7 9 7" />
                  <path d="M5 9.5V20h14V9.5" />
                  <path d="M10 20v-6h4v6" />
                </svg>
                Back to homepage
              </a>
              <a
                href="/contact"
                className="inline-flex items-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-100 focus-visible:outline-none 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 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950"
              >
                Contact support
              </a>
              <button
                type="button"
                onClick={() => window.history.back()}
                className="inline-flex items-center gap-1.5 rounded-xl px-3 py-3 text-sm font-semibold text-slate-500 transition hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:text-slate-400 dark:hover:text-white dark:focus-visible:ring-offset-slate-950"
              >
                <svg
                  viewBox="0 0 24 24"
                  className="h-4 w-4"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  aria-hidden="true"
                >
                  <path d="M19 12H5" />
                  <path d="m11 18-6-6 6-6" />
                </svg>
                Go back
              </button>
            </motion.div>

            {/* Reference code */}
            <motion.div
              {...fadeUp(0.3)}
              className="mt-8 inline-flex items-center gap-3 rounded-xl border border-slate-200 bg-white/70 px-3.5 py-2.5 backdrop-blur dark:border-slate-800 dark:bg-slate-900/60"
            >
              <span className="text-xs font-medium text-slate-500 dark:text-slate-400">
                Reference
              </span>
              <code className="font-mono text-xs font-semibold tracking-tight text-slate-800 dark:text-slate-200">
                {REFERENCE_CODE}
              </code>
              <button
                type="button"
                onClick={() => {
                  void copyReference();
                }}
                aria-describedby={liveId}
                className={`inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-semibold transition 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-900 ${
                  copied
                    ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
                    : "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
                }`}
              >
                {copied ? (
                  <>
                    <svg
                      viewBox="0 0 24 24"
                      className="h-3.5 w-3.5"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2.5"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      aria-hidden="true"
                    >
                      <path d="m20 6-11 11-5-5" />
                    </svg>
                    Copied
                  </>
                ) : (
                  <>
                    <svg
                      viewBox="0 0 24 24"
                      className="h-3.5 w-3.5"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      aria-hidden="true"
                    >
                      <rect x="9" y="9" width="12" height="12" rx="2" />
                      <path d="M5 15V5a2 2 0 0 1 2-2h10" />
                    </svg>
                    Copy
                  </>
                )}
              </button>
              <span id={liveId} role="status" aria-live="polite" className="sr-only">
                {copied ? "Reference code copied to clipboard" : ""}
              </span>
            </motion.div>
          </div>

          {/* Illustration */}
          <div className="order-1 lg:order-2">
            <motion.div
              {...(reduce
                ? { initial: false as const }
                : {
                    initial: { opacity: 0, scale: 0.94 },
                    animate: { opacity: 1, scale: 1 },
                    transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] as const },
                  })}
              onPointerMove={onPointerMove}
              onPointerLeave={onPointerLeave}
              className="relative mx-auto aspect-[4/5] w-full max-w-[440px] overflow-hidden rounded-[2rem] bg-gradient-to-br from-indigo-950 via-violet-950 to-slate-950 ring-1 ring-white/10 shadow-2xl shadow-indigo-950/40"
            >
              <div
                aria-hidden="true"
                className="pointer-events-none absolute inset-0 bg-[radial-gradient(55%_45%_at_50%_25%,rgba(129,140,248,0.28),transparent)]"
              />
              <svg
                viewBox="0 0 420 420"
                className="absolute inset-0 h-full w-full"
                role="img"
                aria-labelledby={`${titleId} ${descId}`}
              >
                <title id={titleId}>Lost astronaut illustration</title>
                <desc id={descId}>
                  An astronaut drifting through space on a tether beside the
                  faded number 404 and a ringed planet.
                </desc>
                <defs>
                  <radialGradient id={`${uid}-visor`} cx="40%" cy="34%" r="78%">
                    <stop offset="0%" stopColor="#7dd3fc" />
                    <stop offset="55%" stopColor="#4338ca" />
                    <stop offset="100%" stopColor="#1e1b4b" />
                  </radialGradient>
                  <radialGradient id={`${uid}-planet`} cx="34%" cy="28%" r="82%">
                    <stop offset="0%" stopColor="#a78bfa" />
                    <stop offset="60%" stopColor="#6d28d9" />
                    <stop offset="100%" stopColor="#4c1d95" />
                  </radialGradient>
                </defs>

                {/* Back layer: faint 404 + far stars */}
                <motion.g style={{ x: starsX, y: starsY }}>
                  <text
                    x="210"
                    y="256"
                    textAnchor="middle"
                    fontSize="196"
                    fontWeight="800"
                    fill="#ffffff"
                    opacity="0.055"
                  >
                    404
                  </text>
                  {STARS.slice(0, 6).map((s, i) => (
                    <circle
                      key={`bs-${i}`}
                      className="err404-twinkle"
                      cx={s.cx}
                      cy={s.cy}
                      r={s.r}
                      fill="#ffffff"
                      style={{ animationDelay: `${s.delay}s` }}
                    />
                  ))}
                </motion.g>

                {/* Planet layer */}
                <motion.g style={{ x: planetX, y: planetY }}>
                  <g className="err404-drift">
                    <circle cx="84" cy="352" r="74" fill={`url(#${uid}-planet)`} />
                    <ellipse
                      cx="84"
                      cy="352"
                      rx="100"
                      ry="21"
                      fill="none"
                      stroke="#818cf8"
                      strokeWidth="4"
                      opacity="0.5"
                      transform="rotate(-18 84 352)"
                    />
                    <circle cx="66" cy="330" r="11" fill="#ffffff" opacity="0.12" />
                    <circle cx="104" cy="368" r="7" fill="#ffffff" opacity="0.1" />
                    <circle cx="52" cy="360" r="5" fill="#ffffff" opacity="0.1" />
                  </g>
                </motion.g>

                {/* Astronaut layer */}
                <motion.g style={{ x: astroX, y: astroY }}>
                  <path
                    d="M210 34 C 252 84, 176 128, 206 168"
                    stroke="#94a3b8"
                    strokeWidth="3"
                    fill="none"
                    strokeLinecap="round"
                    opacity="0.65"
                  />
                  <g className="err404-float">
                    {/* antenna */}
                    <line x1="210" y1="120" x2="210" y2="105" stroke="#94a3b8" strokeWidth="3" />
                    <circle
                      className="err404-twinkle"
                      cx="210"
                      cy="101"
                      r="4"
                      fill="#34d399"
                    />
                    {/* backpack */}
                    <rect x="185" y="150" width="50" height="84" rx="16" fill="#cbd5e1" />
                    {/* arms */}
                    <rect
                      x="156"
                      y="196"
                      width="32"
                      height="16"
                      rx="8"
                      fill="#f1f5f9"
                      transform="rotate(-20 172 204)"
                    />
                    <rect
                      x="232"
                      y="196"
                      width="32"
                      height="16"
                      rx="8"
                      fill="#f1f5f9"
                      transform="rotate(20 248 204)"
                    />
                    <circle cx="156" cy="212" r="9" fill="#6366f1" />
                    <circle cx="264" cy="212" r="9" fill="#6366f1" />
                    {/* legs */}
                    <rect
                      x="188"
                      y="252"
                      width="18"
                      height="36"
                      rx="9"
                      fill="#f1f5f9"
                      transform="rotate(9 197 270)"
                    />
                    <rect
                      x="214"
                      y="252"
                      width="18"
                      height="36"
                      rx="9"
                      fill="#f1f5f9"
                      transform="rotate(-9 223 270)"
                    />
                    <rect
                      x="182"
                      y="280"
                      width="24"
                      height="14"
                      rx="7"
                      fill="#6366f1"
                      transform="rotate(9 194 287)"
                    />
                    <rect
                      x="214"
                      y="280"
                      width="24"
                      height="14"
                      rx="7"
                      fill="#6366f1"
                      transform="rotate(-9 226 287)"
                    />
                    {/* body */}
                    <rect x="181" y="188" width="58" height="74" rx="20" fill="#f8fafc" />
                    <rect x="197" y="208" width="26" height="18" rx="5" fill="#4f46e5" />
                    <circle cx="204" cy="217" r="2.6" fill="#a5b4fc" />
                    <circle cx="216" cy="217" r="2.6" fill="#34d399" />
                    {/* helmet */}
                    <circle cx="210" cy="158" r="41" fill="#f8fafc" />
                    <ellipse cx="210" cy="160" rx="29" ry="27" fill={`url(#${uid}-visor)`} />
                    <ellipse cx="198" cy="149" rx="9" ry="6" fill="#ffffff" opacity="0.55" />
                    <circle cx="222" cy="170" r="3" fill="#ffffff" opacity="0.4" />
                  </g>
                </motion.g>

                {/* Front layer: near stars + orbiting speck */}
                <motion.g style={{ x: frontX, y: frontY }}>
                  {STARS.slice(6).map((s, i) => (
                    <circle
                      key={`fs-${i}`}
                      className="err404-twinkle"
                      cx={s.cx}
                      cy={s.cy}
                      r={s.r}
                      fill="#ffffff"
                      style={{ animationDelay: `${s.delay}s` }}
                    />
                  ))}
                  <g className="err404-orbit">
                    <circle cx="210" cy="52" r="5" fill="#fbbf24" />
                  </g>
                </motion.g>
              </svg>
            </motion.div>
          </div>
        </div>
      </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 →