UI Components Medium
Gauge Meter
A half-circle SVG gauge meter with animated needle, color zones (green/yellow/red), tick marks, a value label, and configurable min/max range. Ideal for dashboards showing a single KPI.
Open in Lab
MCP
svg react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0f1117;
--surface: #16181f;
--surface2: #1e2130;
--border: #2a2d3a;
--text: #e2e8f0;
--text-muted: #64748b;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 40px 24px;
}
.page {
max-width: 780px;
margin: 0 auto;
}
.page-title {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 28px;
}
.gauges-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 24px;
}
@media (max-width: 480px) {
.gauges-grid {
grid-template-columns: 1fr;
}
}
.gauge-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px 16px;
text-align: center;
}
.gauge-wrap {
display: flex;
justify-content: center;
margin-bottom: 12px;
}
.gauge-label {
font-size: 0.82rem;
font-weight: 600;
margin-bottom: 2px;
}
.gauge-unit {
font-size: 0.72rem;
color: var(--text-muted);
}
.gauge-svg {
display: block;
}
.gauge-controls {
display: flex;
justify-content: center;
margin-top: 16px;
}
.ctrl-btn {
background: #818cf8;
border: none;
border-radius: 8px;
color: #fff;
font-size: 0.82rem;
font-weight: 600;
padding: 8px 18px;
cursor: pointer;
font-family: inherit;
transition: background .15s;
}
.ctrl-btn:hover {
background: #a5b4fc;
}
label {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.82rem;
color: var(--text-muted);
}const GAUGES = [
{
id: "gauge1",
value: 68,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 75, color: "#f59e0b" },
{ from: 75, to: 100, color: "#f87171" },
],
},
{
id: "gauge2",
value: 42,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 80, color: "#f59e0b" },
{ from: 80, to: 100, color: "#f87171" },
],
},
{
id: "gauge3",
value: 87,
min: 0,
max: 100,
zones: [
{ from: 0, to: 40, color: "#f87171" },
{ from: 40, to: 70, color: "#f59e0b" },
{ from: 70, to: 100, color: "#34d399" },
],
},
];
const SIZE = 200,
CX = SIZE / 2,
CY = SIZE * 0.6,
R = 80,
STROKE = 16;
const START_ANG = -210 * (Math.PI / 180),
END_ANG = 30 * (Math.PI / 180);
function ptOnArc(r, ang) {
return [CX + r * Math.cos(ang), CY + r * Math.sin(ang)];
}
function arcD(r, a1, a2) {
const [sx, sy] = ptOnArc(r, a1);
const [ex, ey] = ptOnArc(r, a2);
const large = a2 - a1 > Math.PI ? 1 : 0;
return `M${sx.toFixed(2)},${sy.toFixed(2)} A${r},${r} 0 ${large},1 ${ex.toFixed(2)},${ey.toFixed(2)}`;
}
function valToAng(v, min, max) {
return START_ANG + ((v - min) / (max - min)) * (END_ANG - START_ANG);
}
function buildGauge(cfg) {
const wrap = document.getElementById(cfg.id);
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", SIZE);
svg.setAttribute("height", SIZE * 0.7);
svg.setAttribute("viewBox", `0 0 ${SIZE} ${SIZE * 0.7}`);
svg.classList.add("gauge-svg");
// Track
const track = document.createElementNS("http://www.w3.org/2000/svg", "path");
track.setAttribute("d", arcD(R, START_ANG, END_ANG));
track.setAttribute("fill", "none");
track.setAttribute("stroke", "#1e2130");
track.setAttribute("stroke-width", STROKE);
track.setAttribute("stroke-linecap", "round");
svg.appendChild(track);
// Zones
cfg.zones.forEach((z) => {
const a1 = valToAng(z.from, cfg.min, cfg.max);
const a2 = valToAng(z.to, cfg.min, cfg.max);
const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
p.setAttribute("d", arcD(R, a1, a2));
p.setAttribute("fill", "none");
p.setAttribute("stroke", z.color);
p.setAttribute("stroke-width", STROKE * 0.55);
p.setAttribute("stroke-linecap", "round");
svg.appendChild(p);
});
// Min / Max labels
const [mlx, mly] = ptOnArc(R + 18, START_ANG);
const ml = document.createElementNS("http://www.w3.org/2000/svg", "text");
ml.setAttribute("x", mlx);
ml.setAttribute("y", mly);
ml.setAttribute("text-anchor", "middle");
ml.setAttribute("fill", "#64748b");
ml.setAttribute("font-size", "10");
ml.setAttribute("font-family", "inherit");
ml.textContent = cfg.min;
svg.appendChild(ml);
const [mxlx, mxly] = ptOnArc(R + 18, END_ANG);
const mxl = document.createElementNS("http://www.w3.org/2000/svg", "text");
mxl.setAttribute("x", mxlx);
mxl.setAttribute("y", mxly);
mxl.setAttribute("text-anchor", "middle");
mxl.setAttribute("fill", "#64748b");
mxl.setAttribute("font-size", "10");
mxl.setAttribute("font-family", "inherit");
mxl.textContent = cfg.max;
svg.appendChild(mxl);
// Needle
const needleG = document.createElementNS("http://www.w3.org/2000/svg", "g");
const needle = document.createElementNS("http://www.w3.org/2000/svg", "line");
needle.setAttribute("x1", CX);
needle.setAttribute("y1", CY);
needle.setAttribute("stroke", "#e2e8f0");
needle.setAttribute("stroke-width", "2.5");
needle.setAttribute("stroke-linecap", "round");
needleG.appendChild(needle);
// Pivot
const pivot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
pivot.setAttribute("cx", CX);
pivot.setAttribute("cy", CY);
pivot.setAttribute("r", "6");
pivot.setAttribute("fill", "#e2e8f0");
needleG.appendChild(pivot);
svg.appendChild(needleG);
// Value label
const valLbl = document.createElementNS("http://www.w3.org/2000/svg", "text");
valLbl.setAttribute("x", CX);
valLbl.setAttribute("y", CY - 16);
valLbl.setAttribute("text-anchor", "middle");
valLbl.setAttribute("font-size", "22");
valLbl.setAttribute("font-weight", "800");
valLbl.setAttribute("fill", "#e2e8f0");
valLbl.setAttribute("font-family", "inherit");
svg.appendChild(valLbl);
wrap.appendChild(svg);
cfg._needle = needle;
cfg._valLbl = valLbl;
animateGauge(cfg, cfg.value);
}
function setNeedle(cfg, v) {
const ang = valToAng(v, cfg.min, cfg.max);
const [ex, ey] = ptOnArc(R - STROKE / 2 - 2, ang);
cfg._needle.setAttribute("x2", ex.toFixed(2));
cfg._needle.setAttribute("y2", ey.toFixed(2));
cfg._valLbl.textContent = Math.round(v);
}
function animateGauge(cfg, target) {
let start = null;
const duration = 1000;
const from = +(cfg._current || cfg.min);
function step(ts) {
if (!start) start = ts;
const p = Math.min((ts - start) / duration, 1);
const v = from + (target - from) * (1 - Math.pow(1 - p, 3));
setNeedle(cfg, v);
if (p < 1) requestAnimationFrame(step);
else cfg._current = target;
}
requestAnimationFrame(step);
}
GAUGES.forEach(buildGauge);
document.getElementById("randomBtn")?.addEventListener("click", () => {
GAUGES.forEach((g) => animateGauge(g, Math.floor(Math.random() * 91) + 5));
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Gauge Meter</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<h1 class="page-title">Gauge Meters</h1>
<div class="gauges-grid">
<div class="gauge-card">
<div id="gauge1" class="gauge-wrap"></div>
<p class="gauge-label">CPU Temperature</p>
<p class="gauge-unit">°C</p>
</div>
<div class="gauge-card">
<div id="gauge2" class="gauge-wrap"></div>
<p class="gauge-label">Server Load</p>
<p class="gauge-unit">%</p>
</div>
<div class="gauge-card">
<div id="gauge3" class="gauge-wrap"></div>
<p class="gauge-label">Satisfaction</p>
<p class="gauge-unit">/ 100</p>
</div>
</div>
<div class="gauge-controls">
<label>Animate to random values <button class="ctrl-btn" id="randomBtn">Randomize</button></label>
</div>
</main>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect, useRef } from "react";
const SIZE = 200,
CX = SIZE / 2,
CY = SIZE * 0.6,
R = 80,
STROKE = 16;
const START_ANG = -210 * (Math.PI / 180),
END_ANG = 30 * (Math.PI / 180);
type Zone = { from: number; to: number; color: string };
type GaugeCfg = { label: string; value: number; min: number; max: number; zones: Zone[] };
const GAUGES: GaugeCfg[] = [
{
label: "CPU Load",
value: 68,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 75, color: "#f59e0b" },
{ from: 75, to: 100, color: "#f87171" },
],
},
{
label: "Memory",
value: 42,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 80, color: "#f59e0b" },
{ from: 80, to: 100, color: "#f87171" },
],
},
{
label: "Disk Health",
value: 87,
min: 0,
max: 100,
zones: [
{ from: 0, to: 40, color: "#f87171" },
{ from: 40, to: 70, color: "#f59e0b" },
{ from: 70, to: 100, color: "#34d399" },
],
},
];
function ptOnArc(r: number, ang: number): [number, number] {
return [CX + r * Math.cos(ang), CY + r * Math.sin(ang)];
}
function arcD(r: number, a1: number, a2: number) {
const [sx, sy] = ptOnArc(r, a1),
[ex, ey] = ptOnArc(r, a2);
const large = a2 - a1 > Math.PI ? 1 : 0;
return `M${sx.toFixed(2)},${sy.toFixed(2)} A${r},${r} 0 ${large},1 ${ex.toFixed(2)},${ey.toFixed(2)}`;
}
function valToAng(v: number, min: number, max: number) {
return START_ANG + ((v - min) / (max - min)) * (END_ANG - START_ANG);
}
function Gauge({ cfg, value }: { cfg: GaugeCfg; value: number }) {
const [current, setCurrent] = useState(cfg.min);
const rafRef = useRef<number>(0);
const fromRef = useRef(cfg.min);
useEffect(() => {
const from = fromRef.current;
let start: number | null = null;
const duration = 1000;
function step(ts: number) {
if (!start) start = ts;
const p = Math.min((ts - start) / duration, 1);
const eased = 1 - Math.pow(1 - p, 3);
const v = from + (value - from) * eased;
setCurrent(v);
if (p < 1) rafRef.current = requestAnimationFrame(step);
else fromRef.current = value;
}
rafRef.current = requestAnimationFrame(step);
return () => cancelAnimationFrame(rafRef.current);
}, [value]);
const ang = valToAng(current, cfg.min, cfg.max);
const [nx, ny] = ptOnArc(R - STROKE / 2 - 2, ang);
const [mlx, mly] = ptOnArc(R + 18, START_ANG);
const [mxlx, mxly] = ptOnArc(R + 18, END_ANG);
return (
<div className="flex flex-col items-center">
<svg width={SIZE} height={SIZE * 0.7} viewBox={`0 0 ${SIZE} ${SIZE * 0.7}`}>
<path
d={arcD(R, START_ANG, END_ANG)}
fill="none"
stroke="#1e2130"
strokeWidth={STROKE}
strokeLinecap="round"
/>
{cfg.zones.map((z, i) => (
<path
key={i}
d={arcD(R, valToAng(z.from, cfg.min, cfg.max), valToAng(z.to, cfg.min, cfg.max))}
fill="none"
stroke={z.color}
strokeWidth={STROKE * 0.55}
strokeLinecap="round"
/>
))}
<text x={mlx} y={mly} textAnchor="middle" fill="#64748b" fontSize={10}>
{cfg.min}
</text>
<text x={mxlx} y={mxly} textAnchor="middle" fill="#64748b" fontSize={10}>
{cfg.max}
</text>
<line
x1={CX}
y1={CY}
x2={nx.toFixed(2)}
y2={ny.toFixed(2)}
stroke="#e2e8f0"
strokeWidth={2.5}
strokeLinecap="round"
/>
<circle cx={CX} cy={CY} r={6} fill="#e2e8f0" />
<text x={CX} y={CY - 16} textAnchor="middle" fill="#e2e8f0" fontSize={22} fontWeight={800}>
{Math.round(current)}
</text>
</svg>
<div className="text-[#8b949e] text-[13px] -mt-1">{cfg.label}</div>
</div>
);
}
export default function GaugeMeterRC() {
const [values, setValues] = useState(GAUGES.map((g) => g.value));
const randomize = () => setValues(GAUGES.map(() => Math.floor(Math.random() * 91) + 5));
return (
<div className="min-h-screen bg-[#0d1117] p-6 flex flex-col items-center gap-8">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-8">
{GAUGES.map((g, i) => (
<Gauge key={g.label} cfg={g} value={values[i]} />
))}
</div>
<button
onClick={randomize}
className="px-4 py-2 bg-[#818cf8]/20 border border-[#818cf8]/40 text-[#818cf8] rounded-lg text-[13px] hover:bg-[#818cf8]/30 transition-colors"
>
Randomize
</button>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const SIZE = 200;
const CX = SIZE / 2;
const CY = SIZE * 0.6;
const R = 80;
const STROKE = 16;
const START_ANG = -210 * (Math.PI / 180);
const END_ANG = 30 * (Math.PI / 180);
const GAUGES = [
{
label: "CPU Load",
value: 68,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 75, color: "#f59e0b" },
{ from: 75, to: 100, color: "#f87171" },
],
},
{
label: "Memory",
value: 42,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 80, color: "#f59e0b" },
{ from: 80, to: 100, color: "#f87171" },
],
},
{
label: "Disk Health",
value: 87,
min: 0,
max: 100,
zones: [
{ from: 0, to: 40, color: "#f87171" },
{ from: 40, to: 70, color: "#f59e0b" },
{ from: 70, to: 100, color: "#34d399" },
],
},
];
function ptOnArc(r, ang) {
return [CX + r * Math.cos(ang), CY + r * Math.sin(ang)];
}
function arcD(r, a1, a2) {
const [sx, sy] = ptOnArc(r, a1),
[ex, ey] = ptOnArc(r, a2);
const large = a2 - a1 > Math.PI ? 1 : 0;
return `M${sx.toFixed(2)},${sy.toFixed(2)} A${r},${r} 0 ${large},1 ${ex.toFixed(2)},${ey.toFixed(2)}`;
}
function valToAng(v, min, max) {
return START_ANG + ((v - min) / (max - min)) * (END_ANG - START_ANG);
}
const values = ref(GAUGES.map((g) => g.value));
const currents = ref(GAUGES.map((g) => g.min));
const rafIds = [];
function animateGauge(idx, targetVal) {
const cfg = GAUGES[idx];
const from = currents.value[idx];
let start = null;
const duration = 1000;
if (rafIds[idx]) cancelAnimationFrame(rafIds[idx]);
function step(ts) {
if (!start) start = ts;
const p = Math.min((ts - start) / duration, 1);
const eased = 1 - Math.pow(1 - p, 3);
currents.value[idx] = from + (targetVal - from) * eased;
if (p < 1) rafIds[idx] = requestAnimationFrame(step);
}
rafIds[idx] = requestAnimationFrame(step);
}
function randomize() {
values.value = GAUGES.map(() => Math.floor(Math.random() * 91) + 5);
values.value.forEach((v, i) => animateGauge(i, v));
}
function needleXY(idx) {
const ang = valToAng(currents.value[idx], GAUGES[idx].min, GAUGES[idx].max);
return ptOnArc(R - STROKE / 2 - 2, ang);
}
const minLabel = ptOnArc(R + 18, START_ANG);
const maxLabel = ptOnArc(R + 18, END_ANG);
onMounted(() => {
values.value.forEach((v, i) => animateGauge(i, v));
});
onUnmounted(() => {
rafIds.forEach((id) => cancelAnimationFrame(id));
});
</script>
<template>
<div style="min-height:100vh;background:#0d1117;padding:1.5rem;display:flex;flex-direction:column;align-items:center;gap:2rem;font-family:system-ui,-apple-system,sans-serif;">
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:2rem;">
<div v-for="(cfg, i) in GAUGES" :key="cfg.label" style="display:flex;flex-direction:column;align-items:center;">
<svg :width="SIZE" :height="SIZE * 0.7" :viewBox="`0 0 ${SIZE} ${SIZE * 0.7}`">
<path :d="arcD(R, START_ANG, END_ANG)" fill="none" stroke="#1e2130" :stroke-width="STROKE" stroke-linecap="round"/>
<path v-for="(z, zi) in cfg.zones" :key="zi"
:d="arcD(R, valToAng(z.from, cfg.min, cfg.max), valToAng(z.to, cfg.min, cfg.max))"
fill="none" :stroke="z.color" :stroke-width="STROKE * 0.55" stroke-linecap="round"/>
<text :x="minLabel[0]" :y="minLabel[1]" text-anchor="middle" fill="#64748b" font-size="10">{{ cfg.min }}</text>
<text :x="maxLabel[0]" :y="maxLabel[1]" text-anchor="middle" fill="#64748b" font-size="10">{{ cfg.max }}</text>
<line :x1="CX" :y1="CY" :x2="needleXY(i)[0].toFixed(2)" :y2="needleXY(i)[1].toFixed(2)" stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<circle :cx="CX" :cy="CY" r="6" fill="#e2e8f0"/>
<text :x="CX" :y="CY - 16" text-anchor="middle" fill="#e2e8f0" font-size="22" font-weight="800">{{ Math.round(currents[i]) }}</text>
</svg>
<div style="color:#8b949e;font-size:13px;margin-top:-4px;">{{ cfg.label }}</div>
</div>
</div>
<button @click="randomize" style="padding:0.5rem 1rem;background:rgba(129,140,248,0.2);border:1px solid rgba(129,140,248,0.4);color:#818cf8;border-radius:0.5rem;font-size:13px;cursor:pointer;">Randomize</button>
</div>
</template><script>
import { onMount, onDestroy } from "svelte";
const SIZE = 200,
CX = SIZE / 2,
CY = SIZE * 0.6,
R = 80,
STROKE = 16;
const START_ANG = -210 * (Math.PI / 180),
END_ANG = 30 * (Math.PI / 180);
const GAUGES = [
{
label: "CPU Load",
value: 68,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 75, color: "#f59e0b" },
{ from: 75, to: 100, color: "#f87171" },
],
},
{
label: "Memory",
value: 42,
min: 0,
max: 100,
zones: [
{ from: 0, to: 50, color: "#34d399" },
{ from: 50, to: 80, color: "#f59e0b" },
{ from: 80, to: 100, color: "#f87171" },
],
},
{
label: "Disk Health",
value: 87,
min: 0,
max: 100,
zones: [
{ from: 0, to: 40, color: "#f87171" },
{ from: 40, to: 70, color: "#f59e0b" },
{ from: 70, to: 100, color: "#34d399" },
],
},
];
function ptOnArc(r, ang) {
return [CX + r * Math.cos(ang), CY + r * Math.sin(ang)];
}
function arcD(r, a1, a2) {
const [sx, sy] = ptOnArc(r, a1),
[ex, ey] = ptOnArc(r, a2);
const large = a2 - a1 > Math.PI ? 1 : 0;
return `M${sx.toFixed(2)},${sy.toFixed(2)} A${r},${r} 0 ${large},1 ${ex.toFixed(2)},${ey.toFixed(2)}`;
}
function valToAng(v, min, max) {
return START_ANG + ((v - min) / (max - min)) * (END_ANG - START_ANG);
}
let values = GAUGES.map((g) => g.value);
let currents = GAUGES.map((g) => g.min);
let rafIds = [];
function animateGauge(idx, targetVal) {
const cfg = GAUGES[idx];
const from = currents[idx];
let start = null;
const duration = 1000;
if (rafIds[idx]) cancelAnimationFrame(rafIds[idx]);
function step(ts) {
if (!start) start = ts;
const p = Math.min((ts - start) / duration, 1);
const eased = 1 - Math.pow(1 - p, 3);
currents[idx] = from + (targetVal - from) * eased;
currents = currents;
if (p < 1) rafIds[idx] = requestAnimationFrame(step);
}
rafIds[idx] = requestAnimationFrame(step);
}
function randomize() {
values = GAUGES.map(() => Math.floor(Math.random() * 91) + 5);
values.forEach((v, i) => animateGauge(i, v));
}
onMount(() => {
values.forEach((v, i) => animateGauge(i, v));
});
onDestroy(() => {
rafIds.forEach((id) => cancelAnimationFrame(id));
});
function needleXY(idx) {
const ang = valToAng(currents[idx], GAUGES[idx].min, GAUGES[idx].max);
return ptOnArc(R - STROKE / 2 - 2, ang);
}
function minLabelXY() {
return ptOnArc(R + 18, START_ANG);
}
function maxLabelXY() {
return ptOnArc(R + 18, END_ANG);
}
</script>
<div class="gauge-wrapper">
<div class="gauge-grid">
{#each GAUGES as cfg, i}
<div class="gauge-item">
<svg width={SIZE} height={SIZE * 0.7} viewBox="0 0 {SIZE} {SIZE * 0.7}">
<path d={arcD(R, START_ANG, END_ANG)} fill="none" stroke="#1e2130" stroke-width={STROKE} stroke-linecap="round"/>
{#each cfg.zones as z}
<path d={arcD(R, valToAng(z.from, cfg.min, cfg.max), valToAng(z.to, cfg.min, cfg.max))}
fill="none" stroke={z.color} stroke-width={STROKE * 0.55} stroke-linecap="round"/>
{/each}
<text x={minLabelXY()[0]} y={minLabelXY()[1]} text-anchor="middle" fill="#64748b" font-size="10">{cfg.min}</text>
<text x={maxLabelXY()[0]} y={maxLabelXY()[1]} text-anchor="middle" fill="#64748b" font-size="10">{cfg.max}</text>
<line x1={CX} y1={CY} x2={needleXY(i)[0].toFixed(2)} y2={needleXY(i)[1].toFixed(2)} stroke="#e2e8f0" stroke-width="2.5" stroke-linecap="round"/>
<circle cx={CX} cy={CY} r="6" fill="#e2e8f0"/>
<text x={CX} y={CY - 16} text-anchor="middle" fill="#e2e8f0" font-size="22" font-weight="800">{Math.round(currents[i])}</text>
</svg>
<div class="gauge-label">{cfg.label}</div>
</div>
{/each}
</div>
<button class="randomize-btn" on:click={randomize}>Randomize</button>
</div>
<style>
.gauge-wrapper {
min-height: 100vh;
background: #0d1117;
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
.gauge-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
}
.gauge-item {
display: flex;
flex-direction: column;
align-items: center;
}
.gauge-label {
color: #8b949e;
font-size: 13px;
margin-top: -4px;
}
.randomize-btn {
padding: 0.5rem 1rem;
background: rgba(129, 140, 248, 0.2);
border: 1px solid rgba(129, 140, 248, 0.4);
color: #818cf8;
border-radius: 0.5rem;
font-size: 13px;
cursor: pointer;
transition: background 0.15s;
}
.randomize-btn:hover { background: rgba(129, 140, 248, 0.3); }
</style>Features
- Animated needle — smooth rotation via CSS/SVG transform
- Color arc zones — configurable thresholds for green / yellow / red bands
- Tick marks — major and minor tick marks around the arc
- Value label — displays current value + unit below the needle pivot
- Min/Max range — fully configurable numeric range
How it works
- The arc is drawn with SVG
<path>using polar-to-cartesian math - Color zones are separate arc segments rendered in z-order
- The needle is an SVG
<line>rotated viatransform: rotate(Xdeg)around the pivot - Rotation angle maps linearly from minAngle to maxAngle across the value range