Web InnoventixFreeCode

Error Offline

Original · free

offline / no-connection state

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

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

type Status = "offline" | "checking" | "online";

type Step = { title: string; detail: string };
type QueuedItem = { title: string; meta: string; kind: string };
type Cached = { name: string; note: string };

const AUTO_RETRY_SECONDS = 15;

const TROUBLESHOOT: Step[] = [
  {
    title: "Check Wi‑Fi and mobile data",
    detail:
      "Open your device network settings and confirm Wi‑Fi is on and joined to a network, or that mobile data is enabled with usable signal.",
  },
  {
    title: "Turn airplane mode off",
    detail:
      "Airplane mode disables every radio at once. Toggle it off, wait a few seconds, and let the connection re‑establish before retrying.",
  },
  {
    title: "Restart your router",
    detail:
      "Unplug the router, wait a full 30 seconds, then plug it back in. Give it up to a minute to negotiate a fresh connection.",
  },
  {
    title: "Move closer to the access point",
    detail:
      "Thick walls and distance weaken the signal. Step nearer to your router, or to a spot where the network has been reliable before.",
  },
];

const QUEUED: QueuedItem[] = [
  { title: "Reply to Priya Menon", meta: "Draft saved 2 min ago", kind: "Message" },
  { title: "invoice_0426.pdf", meta: "1.2 MB · upload queued", kind: "File" },
];

const CACHED: Cached[] = [
  { name: "Dashboard", note: "Cached 4 min ago" },
  { name: "Saved articles", note: "18 items available" },
  { name: "Account settings", note: "Read‑only offline" },
];

const CSS = `
@keyframes eof-float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} }
@keyframes eof-scan { 0%,100%{opacity:.2} 50%{opacity:1} }
@keyframes eof-spin { to{transform:rotate(360deg)} }
@keyframes eof-ping { 0%{transform:scale(.7);opacity:.55} 100%{transform:scale(2);opacity:0} }
@keyframes eof-bar { 0%,100%{transform:scaleY(.35)} 50%{transform:scaleY(1)} }
@keyframes eof-pop { 0%{transform:scale(.4);opacity:0} 60%{transform:scale(1.08)} 100%{transform:scale(1);opacity:1} }
@keyframes eof-draw { from{stroke-dashoffset:120} to{stroke-dashoffset:0} }
.eof-float{animation:eof-float 6s ease-in-out infinite}
@media (prefers-reduced-motion: reduce){
  .eof-anim, .eof-float{animation:none !important}
}
`;

function fmt(total: number): string {
  const m = Math.floor(total / 60)
    .toString()
    .padStart(2, "0");
  const s = (total % 60).toString().padStart(2, "0");
  return `${m}:${s}`;
}

function IconRefresh({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M3 12a9 9 0 0 1 15.5-6.2L21 8" />
      <path d="M21 3v5h-5" />
      <path d="M21 12a9 9 0 0 1-15.5 6.2L3 16" />
      <path d="M3 21v-5h5" />
    </svg>
  );
}

function IconClock({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <circle cx="12" cy="12" r="9" />
      <path d="M12 7v5l3 2" />
    </svg>
  );
}

function IconChevron({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="m6 9 6 6 6-6" />
    </svg>
  );
}

function IconArrow({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M5 12h14" />
      <path d="m13 6 6 6-6 6" />
    </svg>
  );
}

function IconBolt({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z" />
    </svg>
  );
}

function SignalBars({ status, reduce }: { status: Status; reduce: boolean }) {
  const active = status === "online";
  const scanning = status === "checking";
  return (
    <span className="inline-flex h-4 items-end gap-[3px]" aria-hidden="true">
      {[0, 1, 2, 3].map((i) => (
        <span
          key={i}
          className={`eof-anim w-[3px] origin-bottom rounded-sm ${
            active
              ? "bg-emerald-500"
              : scanning
                ? "bg-indigo-400"
                : "bg-slate-300 dark:bg-slate-700"
          }`}
          style={{
            height: `${6 + i * 3}px`,
            animation: scanning && !reduce ? "eof-bar 1s ease-in-out infinite" : undefined,
            animationDelay: `${i * 0.12}s`,
          }}
        />
      ))}
    </span>
  );
}

export default function ErrorOffline() {
  const reduce = useReducedMotion() ?? false;

  const [status, setStatus] = useState<Status>("offline");
  const [elapsed, setElapsed] = useState(0);
  const [attempts, setAttempts] = useState(0);
  const [autoRetry, setAutoRetry] = useState(true);
  const [countdown, setCountdown] = useState(AUTO_RETRY_SECONDS);
  const [openStep, setOpenStep] = useState<number | null>(0);

  const checkTimer = useRef<number | null>(null);
  const statusRef = useRef<Status>(status);
  const autoRef = useRef<boolean>(autoRetry);
  const runCheckRef = useRef<() => void>(() => {});

  useEffect(() => {
    statusRef.current = status;
  }, [status]);
  useEffect(() => {
    autoRef.current = autoRetry;
  }, [autoRetry]);

  const runCheck = useCallback(() => {
    if (statusRef.current === "checking") return;
    setStatus("checking");
    setAttempts((a) => a + 1);
    if (checkTimer.current !== null) window.clearTimeout(checkTimer.current);
    checkTimer.current = window.setTimeout(
      () => {
        const online = typeof navigator !== "undefined" ? navigator.onLine : false;
        setStatus(online ? "online" : "offline");
        setCountdown(AUTO_RETRY_SECONDS);
        checkTimer.current = null;
      },
      reduce ? 550 : 1700,
    );
  }, [reduce]);

  useEffect(() => {
    runCheckRef.current = runCheck;
  });

  // Real connectivity transitions from the browser.
  useEffect(() => {
    const goOnline = () => setStatus("online");
    const goOffline = () => {
      setStatus("offline");
      setElapsed(0);
      setCountdown(AUTO_RETRY_SECONDS);
    };
    window.addEventListener("online", goOnline);
    window.addEventListener("offline", goOffline);
    return () => {
      window.removeEventListener("online", goOnline);
      window.removeEventListener("offline", goOffline);
    };
  }, []);

  // One ticker: counts offline time and drives the auto‑retry countdown.
  useEffect(() => {
    if (status === "online") return;
    const id = window.setInterval(() => {
      setElapsed((e) => e + 1);
      if (statusRef.current === "offline" && autoRef.current) {
        setCountdown((c) => {
          if (c <= 1) {
            runCheckRef.current();
            return AUTO_RETRY_SECONDS;
          }
          return c - 1;
        });
      }
    }, 1000);
    return () => window.clearInterval(id);
  }, [status]);

  useEffect(
    () => () => {
      if (checkTimer.current !== null) window.clearTimeout(checkTimer.current);
    },
    [],
  );

  const liveMessage =
    status === "online"
      ? "Connection restored. You are back online."
      : status === "checking"
        ? "Checking your connection."
        : "You are offline. Meridian will keep trying to reconnect.";

  const pill =
    status === "online"
      ? { label: "Connected", dot: "bg-emerald-500", ring: "ring-emerald-500/30", text: "text-emerald-700 dark:text-emerald-300", bg: "bg-emerald-50 dark:bg-emerald-500/10" }
      : status === "checking"
        ? { label: "Checking…", dot: "bg-amber-500", ring: "ring-amber-500/30", text: "text-amber-700 dark:text-amber-300", bg: "bg-amber-50 dark:bg-amber-500/10" }
        : { label: "No connection", dot: "bg-rose-500", ring: "ring-rose-500/30", text: "text-rose-700 dark:text-rose-300", bg: "bg-rose-50 dark:bg-rose-500/10" };

  const arcStroke =
    status === "online"
      ? "stroke-emerald-500"
      : status === "checking"
        ? "stroke-indigo-400"
        : "stroke-rose-300 dark:stroke-rose-500/60";

  const arcs = [
    { d: "M63.6 106.5 A20 20 0 0 1 96.4 106.5", delay: "0s" },
    { d: "M50.5 97.4 A36 36 0 0 1 109.5 97.4", delay: "0.15s" },
    { d: "M37.4 88.2 A52 52 0 0 1 122.6 88.2", delay: "0.3s" },
  ];

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
      <style>{CSS}</style>

      {/* decorative atmosphere */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
        <div className="absolute -left-24 -top-16 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-600/20" />
        <div className="absolute -bottom-16 -right-16 h-72 w-72 rounded-full bg-rose-300/25 blur-3xl dark:bg-rose-700/15" />
        <div
          className="absolute inset-0"
          style={{
            backgroundImage:
              "radial-gradient(circle at center, rgba(100,116,139,0.16) 1px, transparent 1.4px)",
            backgroundSize: "22px 22px",
            WebkitMaskImage: "radial-gradient(ellipse at center, black 35%, transparent 78%)",
            maskImage: "radial-gradient(ellipse at center, black 35%, transparent 78%)",
          }}
        />
      </div>

      <p aria-live="polite" className="sr-only">
        {liveMessage}
      </p>

      <div className="relative mx-auto w-full max-w-5xl px-5 py-16 sm:px-8 sm:py-24">
        {/* top bar */}
        <div className="mb-8 flex items-center justify-between gap-4">
          <div className="flex items-center gap-2.5">
            <span className="grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-500/25">
              <IconBolt className="h-5 w-5" />
            </span>
            <span className="text-sm font-semibold tracking-tight">Meridian</span>
          </div>
          <div
            className={`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium ring-1 ${pill.bg} ${pill.text} ${pill.ring}`}
          >
            <span className="relative flex h-2 w-2">
              {status === "offline" && !reduce && (
                <span
                  className="eof-anim absolute inline-flex h-full w-full rounded-full bg-rose-500/60"
                  style={{ animation: "eof-ping 1.8s cubic-bezier(0,0,0.2,1) infinite" }}
                />
              )}
              <span className={`relative inline-flex h-2 w-2 rounded-full ${pill.dot}`} />
            </span>
            {pill.label}
          </div>
        </div>

        {/* hero card */}
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 14 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease: "easeOut" }}
          className="relative overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-xl shadow-slate-900/5 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/30"
        >
          <div className="grid items-center gap-8 p-7 sm:p-10 md:grid-cols-[minmax(0,230px)_1fr]">
            {/* illustration */}
            <div className="mx-auto w-full max-w-[230px]">
              <div className="eof-float relative aspect-square w-full">
                <svg viewBox="0 0 160 160" className="h-full w-full" role="img" aria-label={status === "online" ? "Connection restored" : "No network connection"}>
                  <circle cx="80" cy="80" r="70" className="fill-slate-100 dark:fill-slate-800/60" />
                  <circle cx="80" cy="80" r="70" className="fill-none stroke-slate-200 dark:stroke-slate-700/70" strokeWidth={1} />

                  {status === "online" ? (
                    <g style={{ animation: reduce ? undefined : "eof-pop 0.5s ease-out both", transformOrigin: "80px 80px" }} className="eof-anim">
                      <circle cx="80" cy="80" r="38" className="fill-emerald-500/12 stroke-emerald-500" strokeWidth={2} />
                      <path d="M62 81 74 93 99 66" className="fill-none stroke-emerald-500" strokeWidth={7} strokeLinecap="round" strokeLinejoin="round" />
                    </g>
                  ) : (
                    <>
                      {arcs.map((arc) => (
                        <path
                          key={arc.d}
                          d={arc.d}
                          className={`eof-anim fill-none ${arcStroke}`}
                          strokeWidth={7}
                          strokeLinecap="round"
                          style={{
                            animation:
                              status === "checking" && !reduce
                                ? "eof-scan 1.2s ease-in-out infinite"
                                : undefined,
                            animationDelay: arc.delay,
                            opacity: status === "offline" ? 0.85 : 1,
                          }}
                        />
                      ))}
                      <circle
                        cx="80"
                        cy="118"
                        r="6"
                        className={
                          status === "checking" ? "fill-indigo-400" : "fill-rose-400 dark:fill-rose-500"
                        }
                      />
                      {status === "checking" && !reduce && (
                        <circle
                          cx="80"
                          cy="118"
                          r="6"
                          className="eof-anim fill-none stroke-indigo-400"
                          strokeWidth={2}
                          style={{ animation: "eof-ping 1.4s ease-out infinite", transformOrigin: "80px 118px" }}
                        />
                      )}
                      {status === "offline" && (
                        <line
                          x1="46"
                          y1="46"
                          x2="114"
                          y2="114"
                          className="eof-anim stroke-rose-500"
                          strokeWidth={7}
                          strokeLinecap="round"
                          strokeDasharray="120"
                          style={{ animation: reduce ? undefined : "eof-draw 0.55s ease-out both" }}
                        />
                      )}
                    </>
                  )}
                </svg>
              </div>
            </div>

            {/* copy + actions */}
            <div className="min-w-0">
              {status === "online" ? (
                <>
                  <h1 className="text-2xl font-bold tracking-tight sm:text-3xl">You&rsquo;re back online</h1>
                  <p className="mt-3 max-w-prose text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                    Connection restored after {fmt(elapsed)}. Meridian is syncing your queued
                    changes now &mdash; nothing was lost while you were away.
                  </p>
                  <div className="mt-6 flex flex-wrap items-center gap-3">
                    <button
                      type="button"
                      onClick={() => window.location.reload()}
                      className="inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-emerald-500/25 transition hover:from-emerald-400 hover:to-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"
                    >
                      Reload Meridian
                      <IconArrow className="h-4 w-4" />
                    </button>
                  </div>
                </>
              ) : (
                <>
                  <h1 className="text-2xl font-bold tracking-tight sm:text-3xl">You&rsquo;re offline</h1>
                  <p className="mt-3 max-w-prose text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                    Meridian can&rsquo;t reach the server right now. Your work is saved locally and
                    will sync automatically the moment your connection returns.
                  </p>

                  <div className="mt-6 flex flex-wrap items-center gap-3">
                    <button
                      type="button"
                      onClick={runCheck}
                      disabled={status === "checking"}
                      className="group inline-flex items-center justify-center gap-2 rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-500/25 transition hover:from-indigo-400 hover:to-violet-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-not-allowed disabled:opacity-70 dark:focus-visible:ring-offset-slate-900"
                    >
                      <IconRefresh
                        className={`h-4 w-4 ${
                          status === "checking" && !reduce ? "eof-anim" : ""
                        }`}
                        {...(status === "checking" && !reduce
                          ? { }
                          : {})}
                      />
                      {status === "checking" ? "Checking connection…" : "Try to reconnect"}
                    </button>

                    <span className="text-xs text-slate-500 dark:text-slate-400" aria-live="polite">
                      {status === "checking"
                        ? "Reaching the server…"
                        : autoRetry
                          ? `Next auto attempt in ${countdown}s`
                          : "Auto‑retry is off"}
                    </span>
                  </div>

                  {/* auto retry switch */}
                  <div className="mt-6 flex items-center gap-3 rounded-2xl border border-slate-200 bg-slate-50/70 px-4 py-3 dark:border-slate-800 dark:bg-slate-800/40">
                    <button
                      type="button"
                      role="switch"
                      aria-checked={autoRetry}
                      onClick={() => setAutoRetry((v) => !v)}
                      className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors 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-900 ${
                        autoRetry ? "bg-indigo-500" : "bg-slate-300 dark:bg-slate-700"
                      }`}
                    >
                      <span className="sr-only">Retry automatically</span>
                      <span
                        className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${
                          autoRetry ? "translate-x-5" : "translate-x-0.5"
                        }`}
                      />
                    </button>
                    <div className="min-w-0">
                      <p className="text-sm font-medium">Retry automatically</p>
                      <p className="text-xs text-slate-500 dark:text-slate-400">
                        Reconnect on its own every {AUTO_RETRY_SECONDS} seconds.
                      </p>
                    </div>
                  </div>
                </>
              )}
            </div>
          </div>

          {/* stat strip */}
          <div className="grid grid-cols-3 divide-x divide-slate-200 border-t border-slate-200 dark:divide-slate-800 dark:border-slate-800">
            <div className="flex flex-col gap-1 px-4 py-4 sm:px-8">
              <span className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
                {status === "online" ? "Downtime" : "Offline for"}
              </span>
              <span className="inline-flex items-center gap-1.5 font-mono text-lg font-semibold tabular-nums">
                <IconClock className="h-4 w-4 text-slate-400" />
                {fmt(elapsed)}
              </span>
            </div>
            <div className="flex flex-col gap-1 px-4 py-4 sm:px-8">
              <span className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
                Attempts
              </span>
              <span className="font-mono text-lg font-semibold tabular-nums">{attempts}</span>
            </div>
            <div className="flex flex-col gap-1 px-4 py-4 sm:px-8">
              <span className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
                Signal
              </span>
              <span className="flex items-center gap-2 text-lg font-semibold">
                <SignalBars status={status} reduce={reduce} />
                <span className="text-sm text-slate-500 dark:text-slate-400">
                  {status === "online" ? "Strong" : status === "checking" ? "Scanning" : "None"}
                </span>
              </span>
            </div>
          </div>
        </motion.div>

        {/* supporting grid */}
        <div className="mt-6 grid gap-6 md:grid-cols-3">
          {/* troubleshooting */}
          <motion.div
            initial={reduce ? false : { opacity: 0, y: 14 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.5, ease: "easeOut", delay: reduce ? 0 : 0.08 }}
            className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 md:col-span-2"
          >
            <h2 className="text-base font-semibold tracking-tight">Try these fixes</h2>
            <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
              Most connection drops clear up within a step or two.
            </p>
            <div className="mt-4 divide-y divide-slate-200 dark:divide-slate-800">
              {TROUBLESHOOT.map((step, i) => {
                const open = openStep === i;
                return (
                  <div key={step.title} className="py-1">
                    <h3>
                      <button
                        type="button"
                        aria-expanded={open}
                        aria-controls={`eof-panel-${i}`}
                        id={`eof-tab-${i}`}
                        onClick={() => setOpenStep(open ? null : i)}
                        className="flex w-full items-center gap-3 rounded-lg px-2 py-3 text-left transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:hover:bg-slate-800/60"
                      >
                        <span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-indigo-50 text-xs font-semibold text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-300">
                          {i + 1}
                        </span>
                        <span className="min-w-0 flex-1 text-sm font-medium">{step.title}</span>
                        <IconChevron
                          className={`h-4 w-4 shrink-0 text-slate-400 transition-transform duration-200 ${
                            open ? "rotate-180" : ""
                          }`}
                        />
                      </button>
                    </h3>
                    <div
                      id={`eof-panel-${i}`}
                      role="region"
                      aria-labelledby={`eof-tab-${i}`}
                      hidden={!open}
                      className="px-2 pb-3 pl-12 text-sm leading-relaxed text-slate-600 dark:text-slate-400"
                    >
                      {step.detail}
                    </div>
                  </div>
                );
              })}
            </div>
          </motion.div>

          {/* side column */}
          <motion.div
            initial={reduce ? false : { opacity: 0, y: 14 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.5, ease: "easeOut", delay: reduce ? 0 : 0.16 }}
            className="flex flex-col gap-6"
          >
            {/* queued to sync */}
            <div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
              <div className="flex items-center justify-between">
                <h2 className="text-base font-semibold tracking-tight">Waiting to sync</h2>
                <span className="inline-flex items-center gap-1.5 rounded-full bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700 ring-1 ring-amber-500/20 dark:bg-amber-500/10 dark:text-amber-300">
                  <IconClock className="h-3.5 w-3.5" />
                  {QUEUED.length}
                </span>
              </div>
              <ul className="mt-4 space-y-3">
                {QUEUED.map((item) => (
                  <li key={item.title} className="flex items-start gap-3">
                    <span
                      className="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-amber-400"
                      aria-hidden="true"
                    />
                    <div className="min-w-0">
                      <p className="truncate text-sm font-medium">{item.title}</p>
                      <p className="text-xs text-slate-500 dark:text-slate-400">
                        <span className="font-medium text-slate-400 dark:text-slate-500">
                          {item.kind}
                        </span>{" "}
                        · {item.meta}
                      </p>
                    </div>
                  </li>
                ))}
              </ul>
              <p className="mt-4 text-xs text-slate-500 dark:text-slate-400">
                These will upload automatically once you reconnect.
              </p>
            </div>

            {/* available offline */}
            <div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
              <h2 className="text-base font-semibold tracking-tight">Still available offline</h2>
              <ul className="mt-4 space-y-1.5">
                {CACHED.map((c) => (
                  <li key={c.name}>
                    <button
                      type="button"
                      className="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:hover:bg-slate-800/60"
                    >
                      <span className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-sky-50 text-sky-600 dark:bg-sky-500/15 dark:text-sky-300">
                        <IconArrow className="h-4 w-4" />
                      </span>
                      <span className="min-w-0 flex-1">
                        <span className="block truncate text-sm font-medium">{c.name}</span>
                        <span className="block text-xs text-slate-500 dark:text-slate-400">
                          {c.note}
                        </span>
                      </span>
                      <span className="shrink-0 rounded-md bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-slate-500 dark:bg-slate-800 dark:text-slate-400">
                        Cached
                      </span>
                    </button>
                  </li>
                ))}
              </ul>
            </div>
          </motion.div>
        </div>

        <p className="mt-8 text-center text-xs text-slate-400 dark:text-slate-600">
          Error code <span className="font-mono">NET::ERR_INTERNET_DISCONNECTED</span> · Meridian
          works offline
        </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 →