Web InnoventixFreeCode

Page Overlay Loader

Original · free

full-page loading overlay with progress

byWeb InnoventixReact + Tailwind
loadpageoverlayloaders
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/load-page-overlay.json
load-page-overlay.tsx
"use client";

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

type Speed = "slow" | "normal" | "fast";
type Status = "loading" | "complete";
type Note = { kind: "complete" | "cancel"; text: string } | null;
type Stage = { at: number; label: string; detail: string };

const RATES: Record<Speed, number> = { slow: 15, normal: 33, fast: 66 };

const SPEEDS: { id: Speed; label: string }[] = [
  { id: "slow", label: "Slow" },
  { id: "normal", label: "Normal" },
  { id: "fast", label: "Fast" },
];

const TOTAL_MODULES = 128;

const STAGES: Stage[] = [
  { at: 0, label: "Initializing runtime", detail: "Booting the sandbox container" },
  { at: 12, label: "Establishing connection", detail: "Negotiating a secure TLS session" },
  { at: 30, label: "Fetching modules", detail: "Downloading 128 dependencies" },
  { at: 55, label: "Compiling assets", detail: "Bundling scripts and stylesheets" },
  { at: 78, label: "Optimizing interface", detail: "Tree-shaking and minifying output" },
  { at: 92, label: "Finalizing", detail: "Warming the edge cache" },
];

const READY_STAGE: Stage = {
  at: 100,
  label: "Workspace ready",
  detail: "Everything loaded — jump right in.",
};

const FOCUSABLE =
  'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])';

function stageFor(p: number): Stage {
  let s = STAGES[0];
  for (const st of STAGES) {
    if (p >= st.at) s = st;
  }
  return s;
}

const RING_R = 52;
const RING_C = 2 * Math.PI * RING_R;

export default function LoadPageOverlay() {
  const reduce = useReducedMotion();
  const uid = useId();
  const titleId = `${uid}-title`;
  const descId = `${uid}-desc`;
  const gradId = `${uid}-grad`;

  const [open, setOpen] = useState(false);
  const [status, setStatus] = useState<Status>("loading");
  const [progress, setProgress] = useState(0);
  const [speed, setSpeed] = useState<Speed>("normal");
  const [autoClose, setAutoClose] = useState(true);
  const [note, setNote] = useState<Note>(null);

  const progressRef = useRef(0);
  const startRef = useRef(0);
  const wasOpen = useRef(false);
  const triggerRef = useRef<HTMLButtonElement | null>(null);
  const dialogRef = useRef<HTMLDivElement | null>(null);
  const cancelRef = useRef<HTMLButtonElement | null>(null);
  const enterRef = useRef<HTMLButtonElement | null>(null);

  const start = useCallback(() => {
    progressRef.current = 0;
    startRef.current =
      typeof performance !== "undefined" ? performance.now() : Date.now();
    setProgress(0);
    setNote(null);
    setStatus("loading");
    setOpen(true);
  }, []);

  const cancel = useCallback(() => {
    setOpen(false);
    setStatus("loading");
    progressRef.current = 0;
    setNote({ kind: "cancel", text: "Load cancelled before completion" });
  }, []);

  const enter = useCallback(() => {
    setOpen(false);
  }, []);

  // Progress engine: smooth rAF when motion is allowed, discrete steps otherwise.
  useEffect(() => {
    if (!open || status !== "loading") return;

    if (reduce) {
      const id = window.setInterval(() => {
        const next = Math.min(100, progressRef.current + 12.5);
        progressRef.current = next;
        setProgress(next);
        if (next >= 100) {
          window.clearInterval(id);
          setStatus("complete");
        }
      }, 220);
      return () => window.clearInterval(id);
    }

    let raf = 0;
    let last = performance.now();
    const tick = (now: number) => {
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;
      const p = progressRef.current;
      const ease = 0.35 + 0.65 * ((100 - p) / 100);
      const jitter = 0.8 + Math.random() * 0.4;
      let next = p + RATES[speed] * dt * ease * jitter;
      if (next >= 100) next = 100;
      progressRef.current = next;
      setProgress(next);
      if (next >= 100) {
        setStatus("complete");
        return;
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [open, status, speed, reduce]);

  // On completion: record the elapsed time and optionally auto-dismiss.
  useEffect(() => {
    if (status !== "complete" || !open) return;
    const nowMs =
      typeof performance !== "undefined" ? performance.now() : Date.now();
    const secs = (nowMs - startRef.current) / 1000;
    setNote({ kind: "complete", text: `Workspace ready in ${secs.toFixed(1)}s` });
    if (autoClose) {
      const t = window.setTimeout(() => setOpen(false), 1100);
      return () => window.clearTimeout(t);
    }
  }, [status, open, autoClose]);

  // Move focus into the overlay on open; restore it to the trigger on close.
  useEffect(() => {
    if (open) {
      wasOpen.current = true;
      const t = window.setTimeout(() => cancelRef.current?.focus(), 40);
      return () => window.clearTimeout(t);
    }
    if (wasOpen.current) {
      wasOpen.current = false;
      triggerRef.current?.focus();
    }
  }, [open]);

  // When the run completes, park focus on the confirm button.
  useEffect(() => {
    if (status === "complete" && open) {
      const t = window.setTimeout(() => enterRef.current?.focus(), 40);
      return () => window.clearTimeout(t);
    }
  }, [status, open]);

  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
    if (e.key === "Escape") {
      e.preventDefault();
      cancel();
      return;
    }
    if (e.key !== "Tab" || !dialogRef.current) return;
    const nodes = Array.from(
      dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE),
    ).filter((n) => n.offsetParent !== null);
    if (nodes.length === 0) return;
    const first = nodes[0];
    const last = nodes[nodes.length - 1];
    const active = document.activeElement;
    if (e.shiftKey && active === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && active === last) {
      e.preventDefault();
      first.focus();
    }
  };

  const pct = Math.round(progress);
  const stage = status === "complete" ? READY_STAGE : stageFor(progress);
  const loaded = Math.min(
    TOTAL_MODULES,
    Math.round((progress / 100) * TOTAL_MODULES),
  );
  const liveText =
    status === "complete"
      ? "Loading complete. Your workspace is ready."
      : `${stage.label}…`;
  const ringOffset = RING_C * (1 - progress / 100);

  const ringDuration = reduce ? 0 : 0.28;

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes lpo-spin { to { transform: rotate(360deg); } }
        @keyframes lpo-shimmer { 0% { transform: translateX(-130%); } 100% { transform: translateX(240%); } }
        @keyframes lpo-pulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
        @keyframes lpo-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } }
        .lpo-spin { animation: lpo-spin .95s linear infinite; transform-origin: 60px 60px; }
        .lpo-shimmer { animation: lpo-shimmer 1.5s ease-in-out infinite; }
        .lpo-pulse { animation: lpo-pulse 2.2s ease-in-out infinite; }
        .lpo-float { animation: lpo-float 6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .lpo-spin, .lpo-shimmer, .lpo-pulse, .lpo-float { animation: none !important; }
        }
      `}</style>

      {/* atmosphere */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 opacity-70 [background:radial-gradient(60%_50%_at_15%_0%,theme(colors.indigo.500/0.10),transparent),radial-gradient(50%_50%_at_100%_100%,theme(colors.sky.400/0.10),transparent)]"
      />

      <div className="relative mx-auto max-w-5xl">
        <header className="mb-8 max-w-2xl">
          <p className="mb-3 inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            <span className="inline-block h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Loaders / Overlay
          </p>
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
            Full-page load overlay
          </h2>
          <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            A focus-trapped loading screen with real, staged progress. Trigger a
            simulated deploy, watch it stream through each phase, then cancel or
            step into the workspace when it lands.
          </p>
        </header>

        {/* control bar */}
        <div className="mb-6 flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white/80 p-4 backdrop-blur sm:flex-row sm:flex-wrap sm:items-center sm:justify-between dark:border-slate-800 dark:bg-slate-900/70">
          <div className="flex flex-wrap items-center gap-3">
            <button
              ref={triggerRef}
              type="button"
              onClick={start}
              disabled={open}
              className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors 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-not-allowed disabled:opacity-50 dark:focus-visible:ring-offset-slate-900"
            >
              <BoltIcon className="h-4 w-4" />
              {open ? "Loading…" : "Simulate page load"}
            </button>

            <fieldset
              disabled={open}
              className="flex items-center gap-1 rounded-xl border border-slate-200 bg-slate-100/70 p-1 disabled:opacity-50 dark:border-slate-700 dark:bg-slate-800/60"
            >
              <legend className="sr-only">Load speed</legend>
              {SPEEDS.map((s) => (
                <label
                  key={s.id}
                  className="relative cursor-pointer rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 transition-colors has-[:checked]:bg-white has-[:checked]:text-slate-900 has-[:checked]:shadow-sm dark:text-slate-300 dark:has-[:checked]:bg-slate-950 dark:has-[:checked]:text-white"
                >
                  <input
                    type="radio"
                    name={`${uid}-speed`}
                    value={s.id}
                    checked={speed === s.id}
                    onChange={() => setSpeed(s.id)}
                    className="sr-only [&:focus-visible+span]:outline [&:focus-visible+span]:outline-2 [&:focus-visible+span]:outline-offset-2 [&:focus-visible+span]:outline-indigo-500"
                  />
                  <span className="rounded">{s.label}</span>
                </label>
              ))}
            </fieldset>

            <label className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300">
              <span className="relative inline-flex">
                <input
                  type="checkbox"
                  checked={autoClose}
                  onChange={(e) => setAutoClose(e.target.checked)}
                  className="peer h-5 w-9 cursor-pointer appearance-none rounded-full bg-slate-300 transition-colors checked: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:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
                />
                <span className="pointer-events-none absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white shadow transition-transform peer-checked:translate-x-4" />
              </span>
              Auto-close
            </label>
          </div>

          <p
            aria-live="polite"
            className="min-h-[1.25rem] text-sm text-slate-500 dark:text-slate-400"
          >
            {note?.kind === "complete" && (
              <span className="inline-flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
                <CheckIcon className="h-4 w-4" />
                {note.text}
              </span>
            )}
            {note?.kind === "cancel" && (
              <span className="inline-flex items-center gap-1.5 text-rose-600 dark:text-rose-400">
                <XIcon className="h-4 w-4" />
                {note.text}
              </span>
            )}
          </p>
        </div>

        {/* the "page" being loaded, with the overlay layered on top */}
        <div className="relative min-h-[440px] 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">
          {/* browser chrome */}
          <div className="flex items-center gap-3 border-b border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
            <div className="flex gap-1.5" aria-hidden="true">
              <span className="h-3 w-3 rounded-full bg-rose-400" />
              <span className="h-3 w-3 rounded-full bg-amber-400" />
              <span className="h-3 w-3 rounded-full bg-emerald-400" />
            </div>
            <div className="ml-2 flex-1 truncate rounded-md bg-white px-3 py-1 text-xs text-slate-400 ring-1 ring-inset ring-slate-200 dark:bg-slate-900 dark:text-slate-500 dark:ring-slate-700">
              app.auroraconsole.io/dashboard
            </div>
          </div>

          {/* skeleton dashboard */}
          <div aria-hidden="true" className="space-y-5 p-6 sm:p-8">
            <div className="flex items-center justify-between">
              <div className="space-y-2">
                <div className="h-3 w-40 rounded-full bg-slate-200 dark:bg-slate-800" />
                <div className="h-2.5 w-24 rounded-full bg-slate-100 dark:bg-slate-800/60" />
              </div>
              <div className="h-9 w-9 rounded-full bg-slate-200 dark:bg-slate-800" />
            </div>
            <div className="grid grid-cols-3 gap-3">
              {["Sessions", "Revenue", "Uptime"].map((k) => (
                <div
                  key={k}
                  className="space-y-3 rounded-xl border border-slate-100 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/40"
                >
                  <div className="h-2.5 w-16 rounded-full bg-slate-200 dark:bg-slate-700" />
                  <div className="h-5 w-20 rounded-md bg-slate-200 dark:bg-slate-700" />
                </div>
              ))}
            </div>
            <div className="flex h-32 items-end gap-2 rounded-xl border border-slate-100 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
              {[42, 68, 35, 80, 55, 72, 48, 90, 60, 38, 74, 52].map((h, i) => (
                <div
                  key={i}
                  style={{ height: `${h}%` }}
                  className="flex-1 rounded-t bg-slate-200 dark:bg-slate-700"
                />
              ))}
            </div>
          </div>

          {/* overlay */}
          <AnimatePresence>
            {open && (
              <motion.div
                ref={dialogRef}
                role="dialog"
                aria-modal="true"
                aria-labelledby={titleId}
                aria-describedby={descId}
                onKeyDown={onKeyDown}
                initial={reduce ? { opacity: 1 } : { opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                transition={{ duration: reduce ? 0 : 0.22 }}
                className="absolute inset-0 z-10 flex items-center justify-center bg-white/80 px-6 backdrop-blur-md dark:bg-slate-950/80"
              >
                <div
                  aria-hidden="true"
                  className="pointer-events-none absolute inset-0 [background:radial-gradient(45%_45%_at_50%_35%,theme(colors.indigo.500/0.14),transparent)]"
                />

                <motion.div
                  initial={reduce ? false : { scale: 0.94, opacity: 0, y: 10 }}
                  animate={{ scale: 1, opacity: 1, y: 0 }}
                  exit={reduce ? { opacity: 0 } : { scale: 0.96, opacity: 0, y: 6 }}
                  transition={{ duration: ringDuration, ease: [0.22, 1, 0.36, 1] }}
                  className="relative flex w-full max-w-sm flex-col items-center text-center"
                >
                  <p className="mb-6 inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.25em] text-slate-500 dark:text-slate-400">
                    <span
                      className={`grid h-6 w-6 place-items-center rounded-md bg-indigo-600 text-white ${reduce ? "" : "lpo-pulse"}`}
                    >
                      <BoltIcon className="h-3.5 w-3.5" />
                    </span>
                    Aurora Console
                  </p>

                  {/* progress ring */}
                  <div
                    role="progressbar"
                    aria-valuemin={0}
                    aria-valuemax={100}
                    aria-valuenow={pct}
                    aria-valuetext={`${pct}% — ${stage.label}`}
                    aria-labelledby={titleId}
                    className="relative h-40 w-40"
                  >
                    <svg viewBox="0 0 120 120" className="h-full w-full -rotate-90">
                      <defs>
                        <linearGradient id={gradId} x1="0%" y1="0%" x2="100%" y2="100%">
                          <stop offset="0%" stopColor="#6366f1" />
                          <stop offset="55%" stopColor="#8b5cf6" />
                          <stop offset="100%" stopColor="#38bdf8" />
                        </linearGradient>
                      </defs>
                      <circle
                        cx="60"
                        cy="60"
                        r={RING_R}
                        fill="none"
                        strokeWidth="9"
                        className="stroke-slate-200 dark:stroke-slate-800"
                      />
                      <circle
                        cx="60"
                        cy="60"
                        r={RING_R}
                        fill="none"
                        strokeWidth="9"
                        strokeLinecap="round"
                        stroke={`url(#${gradId})`}
                        strokeDasharray={RING_C}
                        strokeDashoffset={ringOffset}
                      />
                      {status === "loading" && !reduce && (
                        <g className="lpo-spin">
                          <circle
                            cx="60"
                            cy="60"
                            r={RING_R}
                            fill="none"
                            strokeWidth="9"
                            strokeLinecap="round"
                            stroke={`url(#${gradId})`}
                            strokeDasharray={`4 ${RING_C}`}
                            className="opacity-80"
                          />
                        </g>
                      )}
                    </svg>

                    <div className="absolute inset-0 flex flex-col items-center justify-center">
                      {status === "complete" ? (
                        <span className="grid h-12 w-12 place-items-center rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
                          <CheckIcon className="h-7 w-7" />
                        </span>
                      ) : (
                        <>
                          <span className="text-3xl font-semibold tabular-nums tracking-tight text-slate-900 dark:text-white">
                            {pct}
                            <span className="text-lg text-slate-400">%</span>
                          </span>
                          <span className="mt-0.5 text-[11px] font-medium uppercase tracking-wider text-slate-400">
                            loading
                          </span>
                        </>
                      )}
                    </div>
                  </div>

                  <h3
                    id={titleId}
                    className="mt-6 text-xl font-semibold tracking-tight text-slate-900 dark:text-white"
                  >
                    {status === "complete"
                      ? "Workspace ready"
                      : "Preparing your workspace"}
                  </h3>
                  <p
                    id={descId}
                    className="mt-1 min-h-[2.5rem] text-sm text-slate-500 dark:text-slate-400"
                  >
                    {stage.detail}
                  </p>

                  {/* linear bar */}
                  <div className="mt-2 w-full">
                    <div className="relative h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
                      <div
                        className="relative h-full rounded-full bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-400"
                        style={{ width: `${progress}%` }}
                      >
                        {status === "loading" && !reduce && (
                          <span className="lpo-shimmer absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/70 to-transparent" />
                        )}
                      </div>
                    </div>
                    <div className="mt-2 flex items-center justify-between text-xs text-slate-500 dark:text-slate-400">
                      <span className="tabular-nums">
                        {loaded} / {TOTAL_MODULES} modules
                      </span>
                      <span className="inline-flex items-center gap-1 font-medium capitalize">
                        <GaugeIcon className="h-3.5 w-3.5" />
                        {speed}
                      </span>
                    </div>
                  </div>

                  {/* actions */}
                  <div className="mt-7 flex w-full items-center justify-center gap-3">
                    {status === "loading" ? (
                      <button
                        ref={cancelRef}
                        type="button"
                        onClick={cancel}
                        className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 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-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950"
                      >
                        <XIcon className="h-4 w-4" />
                        Cancel
                      </button>
                    ) : (
                      <button
                        ref={enterRef}
                        type="button"
                        onClick={enter}
                        className="inline-flex items-center gap-2 rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors 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-950"
                      >
                        Enter workspace
                        <ArrowRightIcon className="h-4 w-4" />
                      </button>
                    )}
                  </div>

                  {status === "loading" && (
                    <p className="mt-3 text-xs text-slate-400 dark:text-slate-500">
                      Press{" "}
                      <kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 font-sans text-[10px] font-semibold text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
                        Esc
                      </kbd>{" "}
                      to cancel
                    </p>
                  )}
                </motion.div>

                <span className="sr-only" role="status" aria-live="polite">
                  {liveText}
                </span>
              </motion.div>
            )}
          </AnimatePresence>
        </div>

        <p className="mt-5 text-xs text-slate-500 dark:text-slate-500">
          Focus is trapped inside the overlay while it loads, <kbd className="rounded border border-slate-300 bg-slate-100 px-1 py-0.5 font-sans text-[10px] font-semibold text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">Tab</kbd> cycles its controls, and progress steps discretely when reduced motion is on.
        </p>
      </div>
    </section>
  );
}

function BoltIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path
        d="M13 2 4.5 13.5H11l-1 8.5 8.5-11.5H12l1-8.5Z"
        fill="currentColor"
      />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2.5"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="m20 6-11 11-5-5" />
    </svg>
  );
}

function XIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2.2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>
  );
}

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

function GaugeIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M12 14 8 8" />
      <path d="M20.4 16a9 9 0 1 0-16.8 0" />
      <circle cx="12" cy="14" r="1.2" fill="currentColor" stroke="none" />
    </svg>
  );
}

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 →