05半円ゲージ + 針
半円の弧が値の分だけ埋まると同時に針が同じ角度へ実際に回り、+/-ボタンで値を変えると2つが一緒に追従します。
使いどころサーバーCPU・ディスク使用率のように上限がある指標を計器で見せるとき
path arcrotateclick

<main class="stage">
<div class="hg is-demo">
<div class="hg__dial">
<svg class="hg__svg" viewBox="0 0 152 112" width="152" height="112" aria-hidden="true">
<path class="hg__track" d="M14 76A62 62 0 0 1 138 76"/>
<path class="hg__arc" d="M14 76A62 62 0 0 1 138 76"/>
<g class="hg__needle">
<line x1="76" y1="76" x2="76" y2="28"/>
<circle cx="76" cy="76" r="6"/>
</g>
</svg>
<div class="hg__read">
<span class="hg__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="14" height="14"><g fill="currentColor"><path d="m96 240l16-80l-64-24L160 16l-16 80l64 24Z" opacity=".2"/><path d="M215.79 118.17a8 8 0 0 0-5-5.66L153.18 90.9l14.66-73.33a8 8 0 0 0-13.69-7l-112 120a8 8 0 0 0 3 13l57.63 21.61l-14.62 73.25a8 8 0 0 0 13.69 7l112-120a8 8 0 0 0 1.94-7.26M109.37 214l10.47-52.38a8 8 0 0 0-5-9.06L62 132.71l84.62-90.66l-10.46 52.38a8 8 0 0 0 5 9.06l52.8 19.8Z"/></g></svg></span>
<span class="hg__num" role="status" aria-label="CPU使用率"></span>
</div>
</div>
<div class="hg__ctrl">
<button class="hg__btn" type="button" data-step="-10" aria-label="10下げる">−</button>
<span class="hg__cap">CPU使用率</span>
<button class="hg__btn" type="button" data-step="10" aria-label="10上げる">+</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-mint;
@include stage($stage-ink);
@property --num {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
.hg {
--num: 62;
box-sizing: border-box;
display: grid;
justify-items: center;
gap: $sp-3;
color: $subject-cream;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.hg__dial { position: relative; display: grid; place-items: center; }
.hg__svg { width: clamp(132px, 42vw, 152px); height: auto; }
.hg__track {
fill: none;
stroke: rgba(255, 247, 230, .14);
stroke-width: 12;
stroke-linecap: round;
}
.hg__arc {
fill: none;
stroke: $color;
stroke-width: 12;
stroke-linecap: round;
// 반원의 길이 = πr = π × 62 = 194.779. 원 둘레의 딱 절반이다
stroke-dasharray: 194.779;
stroke-dashoffset: calc(194.779 * (1 - var(--num) / 100));
}
.hg__needle {
stroke: $subject-cream;
stroke-width: 3;
stroke-linecap: round;
fill: $subject-cream;
// 0% 는 왼쪽 끝(−90°), 100% 는 오른쪽 끝(+90°) — 180° 를 100 으로 나눠 쓴다
transform: rotate(calc(-90deg + var(--num) * 1.8deg));
transform-origin: 76px 76px; // 규칙8: 다이얼 중심 px 피벗
}
.hg__read {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
gap: $sp-1;
pointer-events: none;
}
.hg__ic { display: inline-flex; color: $color; }
.hg__num {
font-size: clamp(17px, 5.5vw, 20px);
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.hg__num::after {
counter-reset: n var(--num);
content: counter(n) "%";
}
.hg__ctrl { display: flex; align-items: center; gap: $sp-3; }
.hg__cap { font-size: 10px; font-weight: 700; color: rgba(255, 247, 230, .55); }
.hg__btn {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 0;
border-radius: $r-pill;
background: rgba(255, 247, 230, .12);
color: $subject-cream;
font-family: inherit;
font-size: 15px;
font-weight: 700;
line-height: 1;
cursor: pointer;
transition: background $dur-quick $ease-out;
}
.hg__btn:hover, .hg__btn:focus-visible { background: rgba(255, 247, 230, .26); }
// ── 자동 루프: 낮은 값에서 높은 값으로 올라갔다 그대로 내려온다 ──
.hg.is-demo { animation: hg-sweep $duration $easing infinite; }
@keyframes hg-sweep {
0% { --num: 8; }
45%, 55% { --num: 88; }
100% { --num: 8; }
}
@media (max-width: 340px) {
.hg__ctrl { gap: $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.hg.is-demo { animation: none !important; --num: 62; }
.hg__btn { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
@property --num {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
.hg {
--num: 62;
box-sizing: border-box;
display: grid;
justify-items: center;
gap: 12px;
color: #fff7e6;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.hg__dial {
position: relative;
display: grid;
place-items: center;
}
.hg__svg {
width: clamp(132px, 42vw, 152px);
height: auto;
}
.hg__track {
fill: none;
stroke: rgba(255, 247, 230, 0.14);
stroke-width: 12;
stroke-linecap: round;
}
.hg__arc {
fill: none;
stroke: #4cd4a6;
stroke-width: 12;
stroke-linecap: round;
stroke-dasharray: 194.779;
stroke-dashoffset: calc(194.779 * (1 - var(--num) / 100));
}
.hg__needle {
stroke: #fff7e6;
stroke-width: 3;
stroke-linecap: round;
fill: #fff7e6;
transform: rotate(calc(-90deg + var(--num) * 1.8deg));
transform-origin: 76px 76px;
}
.hg__read {
position: absolute;
bottom: 0;
display: flex;
align-items: center;
gap: 4px;
pointer-events: none;
}
.hg__ic {
display: inline-flex;
color: #4cd4a6;
}
.hg__num {
font-size: clamp(17px, 5.5vw, 20px);
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.hg__num::after {
counter-reset: n var(--num);
content: counter(n) "%";
}
.hg__ctrl {
display: flex;
align-items: center;
gap: 12px;
}
.hg__cap {
font-size: 10px;
font-weight: 700;
color: rgba(255, 247, 230, 0.55);
}
.hg__btn {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 0;
border-radius: 999px;
background: rgba(255, 247, 230, 0.12);
color: #fff7e6;
font-family: inherit;
font-size: 15px;
font-weight: 700;
line-height: 1;
cursor: pointer;
transition: background 220ms ease-out;
}
.hg__btn:hover, .hg__btn:focus-visible {
background: rgba(255, 247, 230, 0.26);
}
.hg.is-demo {
animation: hg-sweep 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes hg-sweep {
0% {
--num: 8;
}
45%, 55% {
--num: 88;
}
100% {
--num: 8;
}
}
@media (max-width: 340px) {
.hg__ctrl {
gap: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.hg.is-demo {
animation: none !important;
--num: 62;
}
.hg__btn {
transition: none;
}
}// 실제 동작: --num 하나로 호의 dashoffset 과 바늘의 rotate 를 같이 몬다. 0~100 밖으로는 안 나간다.
var hg = document.querySelector('.hg');
var num = 62;
hg.querySelectorAll('.hg__btn').forEach(function (b) {
b.addEventListener('click', function () {
hg.classList.remove('is-demo');
num = Math.min(100, Math.max(0, num + Number(b.dataset.step)));
hg.style.setProperty('--num', String(num));
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./HalfGaugeNeedle.module.scss";
export type HalfGaugeNeedleProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
"--num"?: string;
};
export default function HalfGaugeNeedle({
duration = 600,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#4cd4a6",
}: HalfGaugeNeedleProps) {
const [num, setNum] = useState(62);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
"--num": String(num),
};
// --num 하나로 호의 dashoffset 과 바늘의 rotate 를 같이 몬다. 0~100 밖으로는 안 나간다
const step = (d: number) => setNum((n) => Math.min(100, Math.max(0, n + d)));
return (
<div className={styles.hg} style={vars}>
<div className={styles.dial}>
<svg className={styles.svg} viewBox="0 0 152 112" width="152" height="112" aria-hidden="true">
<path className={styles.track} d="M14 76A62 62 0 0 1 138 76" />
<path className={styles.arc} d="M14 76A62 62 0 0 1 138 76" />
<g className={styles.needle}>
<line x1="76" y1="76" x2="76" y2="28" />
<circle cx="76" cy="76" r="6" />
</g>
</svg>
<div className={styles.read}>
<span className={styles.ic} aria-hidden="true">
<svg viewBox="0 0 256 256" width="14" height="14">
<g fill="currentColor">
<path d="m96 240l16-80l-64-24L160 16l-16 80l64 24Z" opacity=".2" />
<path d="M215.79 118.17a8 8 0 0 0-5-5.66L153.18 90.9l14.66-73.33a8 8 0 0 0-13.69-7l-112 120a8 8 0 0 0 3 13l57.63 21.61l-14.62 73.25a8 8 0 0 0 13.69 7l112-120a8 8 0 0 0 1.94-7.26M109.37 214l10.47-52.38a8 8 0 0 0-5-9.06L62 132.71l84.62-90.66l-10.46 52.38a8 8 0 0 0 5 9.06l52.8 19.8Z" />
</g>
</svg>
</span>
<span className={styles.num} role="status" aria-label="CPU usage" />
</div>
</div>
<div className={styles.ctrl}>
<button className={styles.btn} type="button" aria-label="Decrease by 10" onClick={() => step(-10)}>
−
</button>
<span className={styles.cap}>CPU usage</span>
<button className={styles.btn} type="button" aria-label="Increase by 10" onClick={() => step(10)}>
+
</button>
</div>
</div>
);
}05 半円ゲージ + 針 — 計測 累積面積 3.285% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · 円形プログレスバー css スタットリング9選 — 数値も実際に動く
このデモが属する記事
円形プログレスバー css スタットリング9選 — 数値も実際に動く円形プログレスバー css とは、棒の代わりに円に沿って値の分だけ埋めていく進捗表示で、SVGの円ひとつに stroke-dasharray と stroke-dashoffset の二つを与えるだけで作れます。九つとも、ボタンやスライダー この記事のデモをすべて見る


