Web InnoventixFreeCode

Semicircle Progress

Original · free

semicircle gauge progress

byWeb InnoventixReact + Tailwind
progsemicircleprogress
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/prog-semicircle.json
prog-semicircle.tsx
"use client";

import { useEffect, useState } from "react";
import {
  motion,
  animate,
  useMotionValue,
  useMotionValueEvent,
  useReducedMotion,
} from "motion/react";

type Zone = "healthy" | "warm" | "critical";

const ZONES: Record<
  Zone,
  { label: string; arcText: string; pillClass: string; sliderAccent: string }
> = {
  healthy: {
    label: "Comfortable headroom",
    arcText: "text-emerald-500 dark:text-emerald-400",
    pillClass:
      "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
    sliderAccent: "accent-emerald-500",
  },
  warm: {
    label: "Running warm",
    arcText: "text-amber-500 dark:text-amber-400",
    pillClass:
      "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
    sliderAccent: "accent-amber-500",
  },
  critical: {
    label: "Near saturation",
    arcText: "text-rose-500 dark:text-rose-400",
    pillClass:
      "bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
    sliderAccent: "accent-rose-500",
  },
};

const PRESETS: { label: string; v: number }[] = [
  { label: "Idle", v: 24 },
  { label: "Steady", v: 52 },
  { label: "Peak hour", v: 78 },
  { label: "Incident", v: 94 },
];

const CX = 160;
const CY = 150;
const R = 120;
const ARC = `M ${CX - R} ${CY} A ${R} ${R} 0 0 1 ${CX + R} ${CY}`;

function clamp(n: number): number {
  return Math.max(0, Math.min(100, n));
}

function zoneOf(v: number): Zone {
  return v < 60 ? "healthy" : v < 85 ? "warm" : "critical";
}

function polar(v: number): { x: number; y: number } {
  const t = clamp(v) / 100;
  const a = Math.PI - t * Math.PI;
  return { x: CX + R * Math.cos(a), y: CY - R * Math.sin(a) };
}

function radial(
  v: number,
  r1: number,
  r2: number,
): { x1: number; y1: number; x2: number; y2: number } {
  const t = clamp(v) / 100;
  const a = Math.PI - t * Math.PI;
  return {
    x1: CX + r1 * Math.cos(a),
    y1: CY - r1 * Math.sin(a),
    x2: CX + r2 * Math.cos(a),
    y2: CY - r2 * Math.sin(a),
  };
}

export default function ProgSemicircle() {
  const reduce = !!useReducedMotion();
  const [value, setValue] = useState(52);
  const [display, setDisplay] = useState(0);

  const p = useMotionValue(0);
  useMotionValueEvent(p, "change", (latest) => setDisplay(latest));

  useEffect(() => {
    if (reduce) {
      p.set(value);
      return;
    }
    const controls = animate(p, value, {
      duration: 0.85,
      ease: [0.22, 1, 0.36, 1],
    });
    return () => controls.stop();
  }, [value, reduce, p]);

  const shown = clamp(display);
  const zone = zoneOf(shown);
  const meta = ZONES[zone];
  const dot = polar(shown);
  const big = Math.round(shown);

  const nodes = value < 60 ? 6 : value < 85 ? 8 : 10;
  const rps = Math.round(value * 41.6);
  const p95 = Math.round(46 + Math.pow(value / 100, 3) * 680);

  const t60 = radial(60, R - 11, R + 11);
  const t85 = radial(85, R - 11, R + 11);

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-6 py-16 sm:py-24 dark:bg-slate-950">
      <style>{`
        @keyframes psg-halo { 0%,100%{ opacity:.28; transform:scale(1); } 50%{ opacity:.06; transform:scale(2); } }
        @keyframes psg-live { 0%{ opacity:.9; transform:scale(1); } 70%,100%{ opacity:0; transform:scale(2.6); } }
        .psg-halo{ transform-box:fill-box; transform-origin:center; animation:psg-halo 2.4s ease-in-out infinite; }
        .psg-live{ transform-origin:center; animation:psg-live 1.8s ease-out infinite; }
        @media (prefers-reduced-motion: reduce){ .psg-halo,.psg-live{ animation:none !important; } }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <div className="mb-8 text-center sm:mb-10">
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400">
            Live infrastructure
          </p>
          <h2 className="mt-2 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
            Cluster utilization
          </h2>
          <p className="mt-2 text-sm text-slate-600 dark:text-slate-400">
            Compute pressure across the us-east-2 node pool, sampled every 30
            seconds.
          </p>
        </div>

        <motion.div
          initial={reduce ? false : { opacity: 0, y: 12 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
          className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm sm:p-9 dark:border-slate-800 dark:bg-slate-900"
        >
          <div className="flex items-center justify-between gap-3">
            <div className="flex items-center gap-2 text-xs font-medium text-slate-500 dark:text-slate-400">
              <span className="relative flex h-2 w-2">
                <span className="psg-live absolute inline-flex h-full w-full rounded-full bg-emerald-500" />
                <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
              </span>
              <span>us-east-2 &middot; node pool</span>
            </div>
            <span
              className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold ${meta.pillClass}`}
            >
              <span className="h-1.5 w-1.5 rounded-full bg-current" />
              {meta.label}
            </span>
          </div>

          <p className="sr-only" aria-live="polite">
            Cluster utilization {value} percent. {ZONES[zoneOf(value)].label}.
          </p>

          <div className="relative mx-auto mt-4 max-w-md">
            <svg
              viewBox="0 0 320 176"
              className="h-auto w-full overflow-visible"
              role="img"
              aria-label={`Semicircle gauge showing ${value}% cluster utilization`}
            >
              <path
                d={ARC}
                fill="none"
                strokeWidth={18}
                strokeLinecap="round"
                stroke="currentColor"
                className="text-slate-200 dark:text-slate-800"
              />
              <path
                d={ARC}
                fill="none"
                strokeWidth={18}
                strokeLinecap="round"
                stroke="currentColor"
                pathLength={100}
                strokeDasharray={100}
                strokeDashoffset={100 - shown}
                className={meta.arcText}
              />

              <g
                stroke="currentColor"
                strokeWidth={3}
                strokeLinecap="round"
                className="text-white dark:text-slate-900"
              >
                <line x1={t60.x1} y1={t60.y1} x2={t60.x2} y2={t60.y2} />
                <line x1={t85.x1} y1={t85.y1} x2={t85.x2} y2={t85.y2} />
              </g>

              <g className={meta.arcText}>
                <circle
                  className="psg-halo"
                  cx={dot.x}
                  cy={dot.y}
                  r={9}
                  fill="currentColor"
                />
                <circle
                  cx={dot.x}
                  cy={dot.y}
                  r={9}
                  fill="currentColor"
                  className="text-white dark:text-slate-900"
                />
                <circle cx={dot.x} cy={dot.y} r={6} fill="currentColor" />
              </g>

              <text
                x={40}
                y={172}
                textAnchor="middle"
                fontSize={11}
                fontWeight={600}
                className="fill-slate-400 dark:fill-slate-500"
              >
                0
              </text>
              <text
                x={280}
                y={172}
                textAnchor="middle"
                fontSize={11}
                fontWeight={600}
                className="fill-slate-400 dark:fill-slate-500"
              >
                100
              </text>
            </svg>

            <div className="pointer-events-none absolute left-1/2 top-[62%] -translate-x-1/2 -translate-y-1/2 text-center">
              <div className="flex items-start justify-center">
                <span className="text-5xl font-semibold tabular-nums tracking-tight text-slate-900 sm:text-6xl dark:text-white">
                  {big}
                </span>
                <span className="mt-1 text-2xl font-semibold text-slate-400 sm:mt-1.5 dark:text-slate-500">
                  %
                </span>
              </div>
              <p className="mt-1 text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                of provisioned vCPU
              </p>
            </div>
          </div>

          <dl className="mt-6 grid grid-cols-3 gap-px overflow-hidden rounded-2xl border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-800">
            <div className="bg-white p-3 sm:p-4 dark:bg-slate-900">
              <dt className="text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                Active nodes
              </dt>
              <dd className="mt-1 text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
                {nodes}
              </dd>
            </div>
            <div className="bg-white p-3 sm:p-4 dark:bg-slate-900">
              <dt className="text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                Requests / s
              </dt>
              <dd className="mt-1 text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
                {rps.toLocaleString()}
              </dd>
            </div>
            <div className="bg-white p-3 sm:p-4 dark:bg-slate-900">
              <dt className="text-[11px] font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
                p95 latency
              </dt>
              <dd className="mt-1 text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
                {p95} ms
              </dd>
            </div>
          </dl>

          <div className="mt-8 space-y-5">
            <div>
              <div className="mb-2.5 flex items-center justify-between">
                <span className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                  Scenarios
                </span>
                <button
                  type="button"
                  onClick={() => setValue(52)}
                  className="rounded text-xs font-medium text-slate-500 underline-offset-4 transition hover:text-slate-800 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-slate-400 dark:hover:text-slate-200 dark:focus-visible:ring-slate-500 dark:focus-visible:ring-offset-slate-900"
                >
                  Reset
                </button>
              </div>
              <div
                role="group"
                aria-label="Load scenarios"
                className="flex flex-wrap gap-2"
              >
                {PRESETS.map((preset) => {
                  const active = value === preset.v;
                  return (
                    <button
                      key={preset.label}
                      type="button"
                      aria-pressed={active}
                      onClick={() => setValue(preset.v)}
                      className={`rounded-full px-3.5 py-1.5 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-slate-500 dark:focus-visible:ring-offset-slate-900 ${
                        active
                          ? "border border-transparent bg-slate-900 text-white dark:bg-white dark:text-slate-900"
                          : "border border-slate-300 bg-white text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
                      }`}
                    >
                      {preset.label}
                    </button>
                  );
                })}
              </div>
            </div>

            <div>
              <div className="mb-2 flex items-center justify-between">
                <label
                  htmlFor="psg-load"
                  className="text-xs font-medium text-slate-600 dark:text-slate-400"
                >
                  Simulated load
                </label>
                <span className="text-xs font-semibold tabular-nums text-slate-700 dark:text-slate-300">
                  {value}%
                </span>
              </div>
              <div className="flex items-center gap-3">
                <button
                  type="button"
                  onClick={() => setValue((v) => clamp(v - 5))}
                  disabled={value <= 0}
                  aria-label="Decrease load by 5 percent"
                  className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-300 bg-white text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-slate-500 dark:focus-visible:ring-offset-slate-900"
                >
                  <svg viewBox="0 0 20 20" className="h-5 w-5" aria-hidden="true">
                    <path
                      d="M5 10h10"
                      stroke="currentColor"
                      strokeWidth={2}
                      strokeLinecap="round"
                    />
                  </svg>
                </button>
                <input
                  id="psg-load"
                  type="range"
                  min={0}
                  max={100}
                  step={1}
                  value={value}
                  onChange={(e) => setValue(Number(e.currentTarget.value))}
                  aria-label="Set cluster utilization percentage"
                  className={`h-2 flex-1 cursor-pointer rounded ${meta.sliderAccent} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-slate-500 dark:focus-visible:ring-offset-slate-900`}
                />
                <button
                  type="button"
                  onClick={() => setValue((v) => clamp(v + 5))}
                  disabled={value >= 100}
                  aria-label="Increase load by 5 percent"
                  className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-300 bg-white text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-40 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-slate-500 dark:focus-visible:ring-offset-slate-900"
                >
                  <svg viewBox="0 0 20 20" className="h-5 w-5" aria-hidden="true">
                    <path
                      d="M10 5v10M5 10h10"
                      stroke="currentColor"
                      strokeWidth={2}
                      strokeLinecap="round"
                    />
                  </svg>
                </button>
              </div>
            </div>
          </div>

          <div className="mt-6 flex items-start gap-2 rounded-xl bg-slate-50 p-3 text-xs text-slate-500 dark:bg-slate-800/60 dark:text-slate-400">
            <svg
              viewBox="0 0 20 20"
              className="mt-0.5 h-4 w-4 shrink-0 text-slate-400 dark:text-slate-500"
              aria-hidden="true"
            >
              <path
                d="M11 2 4 11h5l-1 7 7-9h-5l1-7Z"
                fill="currentColor"
              />
            </svg>
            <p>
              Autoscaling triggers at{" "}
              <span className="font-semibold text-slate-700 dark:text-slate-200">
                85%
              </span>
              . New nodes join the pool in ~40s; current p95 latency is {p95} ms.
            </p>
          </div>
        </motion.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 →