円形プログレスゲージ
円のstroke-dashoffsetで進捗を可視化し、中央の数値をカウントアップ。ダッシュボードやスキル表示に使えます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:KPIダッシュボード。円形プログレスゲージが主役 -->
<section class="fd-dash">
<header class="fd-dash__head">
<h2 class="fd-dash__title">今月のチーム目標</h2>
<span class="fd-dash__period">2026年6月 ・ 自動集計</span>
</header>
<div class="fd-dash__body">
<!-- 主役:中央の大ゲージ -->
<div class="fd-gauge">
<svg viewBox="0 0 120 120" role="img" aria-label="達成率ゲージ">
<defs>
<linearGradient id="fdRing" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#4f7cff" />
<stop offset="100%" stop-color="#8fb0ff" />
</linearGradient>
</defs>
<circle class="fd-track" cx="60" cy="60" r="52" />
<circle class="fd-bar" cx="60" cy="60" r="52" />
</svg>
<div class="fd-gauge__readout">
<span class="fd-gauge__num" id="fdPct">0</span><span class="fd-gauge__unit">%</span>
<span class="fd-gauge__label">達成率</span>
</div>
</div>
<!-- 内訳のミニ指標 -->
<ul class="fd-stats">
<li><b>284</b><span>新規商談</span></li>
<li><b>¥4.2M</b><span>受注額</span></li>
<li><b>92%</b><span>継続率</span></li>
</ul>
</div>
<div class="fd-dash__ctrl">
<span class="fd-dash__ctrlLabel">表示する四半期</span>
<div class="fd-seg">
<button class="fd-seg__btn" data-v="42" type="button">Q1</button>
<button class="fd-seg__btn is-active" data-v="76" type="button">Q2</button>
<button class="fd-seg__btn" data-v="100" type="button">通期</button>
</div>
</div>
</section>
CSS
/* FlowDesk:KPIダッシュボード(円形プログレスゲージ) */
:root {
--navy: #0f1b34;
--blue: #4f7cff;
--line: #e7ecf6;
--text: #46506b;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
display: grid;
place-items: center;
background: #eef2fb;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--text);
overflow: hidden;
}
.fd-dash {
width: min(580px, 94vw);
padding: 20px 24px 18px;
background: #fff;
border-radius: 20px;
box-shadow: 0 22px 50px -24px rgba(15, 27, 52, 0.4);
}
.fd-dash__head { display: flex; align-items: baseline; justify-content: space-between; }
.fd-dash__title { margin: 0; font-size: 17px; font-weight: 800; color: var(--navy); }
.fd-dash__period { font-size: 11px; color: #9aa3bd; }
.fd-dash__body {
display: grid;
grid-template-columns: 130px 1fr;
align-items: center;
gap: 22px;
margin: 16px 0 14px;
}
/* 主役ゲージ */
.fd-gauge { position: relative; width: 130px; height: 130px; }
.fd-gauge svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.fd-track { fill: none; stroke: var(--line); stroke-width: 12; }
.fd-bar {
fill: none;
stroke: url(#fdRing);
stroke-width: 12;
stroke-linecap: round;
transition: stroke-dashoffset 1s cubic-bezier(0.22, 1, 0.36, 1);
}
.fd-gauge__readout {
position: absolute;
inset: 0;
display: grid;
align-content: center;
justify-items: center;
line-height: 1;
}
.fd-gauge__num { font-size: 34px; font-weight: 800; color: var(--navy); }
.fd-gauge__unit { font-size: 14px; font-weight: 700; color: var(--blue); margin-top: 2px; }
.fd-gauge__label { margin-top: 6px; font-size: 11px; color: #9aa3bd; }
/* 内訳ミニ指標 */
.fd-stats { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.fd-stats li {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 9px 14px;
border-radius: 12px;
background: #f5f7fc;
}
.fd-stats b { font-size: 16px; color: var(--navy); }
.fd-stats span { font-size: 12px; color: var(--text); }
/* 四半期セグメント */
.fd-dash__ctrl { display: flex; align-items: center; justify-content: space-between; }
.fd-dash__ctrlLabel { font-size: 11px; color: #9aa3bd; }
.fd-seg {
display: inline-flex;
padding: 3px;
border-radius: 999px;
background: #eef2fb;
}
.fd-seg__btn {
border: none;
background: transparent;
padding: 6px 14px;
border-radius: 999px;
font: 700 12px/1 "Segoe UI", sans-serif;
color: var(--text);
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.fd-seg__btn:hover { color: var(--navy); }
.fd-seg__btn.is-active { background: var(--blue); color: #fff; box-shadow: 0 4px 10px rgba(79, 124, 255, 0.4); }
@media (prefers-reduced-motion: reduce) {
.fd-bar { transition: none; }
.fd-seg__btn { transition: none; }
}
JavaScript
// 円形ゲージ:dashoffsetでリングを進め、数値をカウントアップ。四半期で切替
const bar = document.querySelector(".fd-bar");
const pctEl = document.getElementById("fdPct");
const buttons = document.querySelectorAll(".fd-seg__btn");
if (bar && pctEl) {
const R = 52;
const CIRC = 2 * Math.PI * R; // 円周
bar.style.strokeDasharray = CIRC.toFixed(1);
bar.style.strokeDashoffset = CIRC.toFixed(1); // 初期は0%
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let countTimer = null;
// 進捗(0-100)をリングと数字へ反映
const setProgress = (target) => {
const value = Math.max(0, Math.min(100, target));
bar.style.strokeDashoffset = (CIRC * (1 - value / 100)).toFixed(1);
if (countTimer) cancelAnimationFrame(countTimer);
const from = parseInt(pctEl.textContent, 10) || 0;
if (reduce) { pctEl.textContent = value; return; }
const start = performance.now();
const DUR = 1000;
const step = (now) => {
const t = Math.min((now - start) / DUR, 1);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
pctEl.textContent = Math.round(from + (value - from) * eased);
if (t < 1) countTimer = requestAnimationFrame(step);
};
countTimer = requestAnimationFrame(step);
};
// セグメントボタン:選択状態と進捗を同期
buttons.forEach((b) => {
b.addEventListener("click", () => {
buttons.forEach((x) => x.classList.remove("is-active"));
b.classList.add("is-active");
setProgress(Number(b.dataset.v));
});
});
// 初期演出:少し待ってからQ2(76%)へ
setTimeout(() => setProgress(76), 350);
}
コード
HTML
<!-- 円形プログレスゲージ: stroke-dashoffsetで進捗を表現、数値はJSでカウントアップ -->
<div class="gauge-stage">
<div class="gauge">
<svg viewBox="0 0 120 120" role="img" aria-label="進捗ゲージ">
<defs>
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22d3ee" />
<stop offset="100%" stop-color="#a855f7" />
</linearGradient>
</defs>
<!-- 背景トラック -->
<circle class="track" cx="60" cy="60" r="52" />
<!-- 進捗リング(JSでoffset設定) -->
<circle class="bar" cx="60" cy="60" r="52" />
</svg>
<!-- 中央の数値 -->
<div class="readout">
<span class="num" id="pct">0</span><span class="unit">%</span>
</div>
</div>
<div class="controls">
<button class="btn" data-v="25" type="button">25%</button>
<button class="btn" data-v="68" type="button">68%</button>
<button class="btn" data-v="100" type="button">100%</button>
</div>
</div>
CSS
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background: radial-gradient(120% 120% at 50% 0%, #1e293b 0%, #0f172a 60%, #020617 100%);
}
.gauge-stage {
display: grid;
justify-items: center;
gap: 22px;
}
.gauge {
position: relative;
width: min(56vw, 200px);
aspect-ratio: 1;
}
.gauge svg {
width: 100%; height: 100%;
/* 12時方向を始点にするため反時計回りに90度回す */
transform: rotate(-90deg);
}
.track {
fill: none;
stroke: rgba(255, 255, 255, .08);
stroke-width: 12;
}
.bar {
fill: none;
stroke: url(#ring);
stroke-width: 12;
stroke-linecap: round;
/* dasharray=円周, offsetをJSで変えて進捗を出す */
stroke-dasharray: 327; /* 2πr (r=52) ≈ 326.7 */
stroke-dashoffset: 327;
transition: stroke-dashoffset 1.1s cubic-bezier(.22, 1, .36, 1);
filter: drop-shadow(0 0 6px rgba(168, 85, 247, .5));
}
.readout {
position: absolute;
inset: 0;
display: grid;
place-content: center;
/* svgの回転に影響されない */
color: #f8fafc;
font-variant-numeric: tabular-nums;
}
.num { font-size: clamp(28px, 9vw, 44px); font-weight: 800; }
.unit { font-size: 16px; opacity: .65; margin-left: 2px; }
.controls { display: flex; gap: 10px; }
.btn {
font: 600 13px/1 "Segoe UI", sans-serif;
color: #e2e8f0;
background: rgba(148, 163, 184, .12);
border: 1px solid rgba(148, 163, 184, .3);
padding: 8px 14px;
border-radius: 10px;
cursor: pointer;
transition: background .2s, transform .08s, border-color .2s;
}
.btn:hover { background: rgba(168, 85, 247, .25); border-color: rgba(168, 85, 247, .6); }
.btn:active { transform: scale(.95); }
@media (prefers-reduced-motion: reduce) {
.bar { transition: none; }
}
JavaScript
// 円形ゲージ: dashoffsetでリングを進め、数値をカウントアップ表示
const bar = document.querySelector(".bar");
const pctEl = document.getElementById("pct");
const buttons = document.querySelectorAll(".btn");
if (bar && pctEl) {
const R = 52;
const CIRC = 2 * Math.PI * R; // 円周
bar.style.strokeDasharray = CIRC.toFixed(1);
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let countTimer = null;
// 進捗(0-100)を反映。リングのoffsetと数字を同期
const setProgress = (target) => {
const value = Math.max(0, Math.min(100, target));
// リング: 100%でoffset=0
bar.style.strokeDashoffset = (CIRC * (1 - value / 100)).toFixed(1);
// 数値カウントアップ
if (countTimer) cancelAnimationFrame(countTimer);
const from = parseInt(pctEl.textContent, 10) || 0;
if (reduce) { pctEl.textContent = value; return; }
const start = performance.now();
const DUR = 1000;
const step = (now) => {
const t = Math.min((now - start) / DUR, 1);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
pctEl.textContent = Math.round(from + (value - from) * eased);
if (t < 1) countTimer = requestAnimationFrame(step);
};
countTimer = requestAnimationFrame(step);
};
buttons.forEach((b) => {
b.addEventListener("click", () => setProgress(Number(b.dataset.v)));
});
// 初期演出: 少し待ってから68%へ
setTimeout(() => setProgress(68), 350);
}
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「円形プログレスゲージ」の効果を追加してください。
# 追加してほしい効果
円形プログレスゲージ(SVG エフェクト)
円のstroke-dashoffsetで進捗を可視化し、中央の数値をカウントアップ。ダッシュボードやスキル表示に使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 円形プログレスゲージ: stroke-dashoffsetで進捗を表現、数値はJSでカウントアップ -->
<div class="gauge-stage">
<div class="gauge">
<svg viewBox="0 0 120 120" role="img" aria-label="進捗ゲージ">
<defs>
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22d3ee" />
<stop offset="100%" stop-color="#a855f7" />
</linearGradient>
</defs>
<!-- 背景トラック -->
<circle class="track" cx="60" cy="60" r="52" />
<!-- 進捗リング(JSでoffset設定) -->
<circle class="bar" cx="60" cy="60" r="52" />
</svg>
<!-- 中央の数値 -->
<div class="readout">
<span class="num" id="pct">0</span><span class="unit">%</span>
</div>
</div>
<div class="controls">
<button class="btn" data-v="25" type="button">25%</button>
<button class="btn" data-v="68" type="button">68%</button>
<button class="btn" data-v="100" type="button">100%</button>
</div>
</div>
【CSS】
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background: radial-gradient(120% 120% at 50% 0%, #1e293b 0%, #0f172a 60%, #020617 100%);
}
.gauge-stage {
display: grid;
justify-items: center;
gap: 22px;
}
.gauge {
position: relative;
width: min(56vw, 200px);
aspect-ratio: 1;
}
.gauge svg {
width: 100%; height: 100%;
/* 12時方向を始点にするため反時計回りに90度回す */
transform: rotate(-90deg);
}
.track {
fill: none;
stroke: rgba(255, 255, 255, .08);
stroke-width: 12;
}
.bar {
fill: none;
stroke: url(#ring);
stroke-width: 12;
stroke-linecap: round;
/* dasharray=円周, offsetをJSで変えて進捗を出す */
stroke-dasharray: 327; /* 2πr (r=52) ≈ 326.7 */
stroke-dashoffset: 327;
transition: stroke-dashoffset 1.1s cubic-bezier(.22, 1, .36, 1);
filter: drop-shadow(0 0 6px rgba(168, 85, 247, .5));
}
.readout {
position: absolute;
inset: 0;
display: grid;
place-content: center;
/* svgの回転に影響されない */
color: #f8fafc;
font-variant-numeric: tabular-nums;
}
.num { font-size: clamp(28px, 9vw, 44px); font-weight: 800; }
.unit { font-size: 16px; opacity: .65; margin-left: 2px; }
.controls { display: flex; gap: 10px; }
.btn {
font: 600 13px/1 "Segoe UI", sans-serif;
color: #e2e8f0;
background: rgba(148, 163, 184, .12);
border: 1px solid rgba(148, 163, 184, .3);
padding: 8px 14px;
border-radius: 10px;
cursor: pointer;
transition: background .2s, transform .08s, border-color .2s;
}
.btn:hover { background: rgba(168, 85, 247, .25); border-color: rgba(168, 85, 247, .6); }
.btn:active { transform: scale(.95); }
@media (prefers-reduced-motion: reduce) {
.bar { transition: none; }
}
【JavaScript】
// 円形ゲージ: dashoffsetでリングを進め、数値をカウントアップ表示
const bar = document.querySelector(".bar");
const pctEl = document.getElementById("pct");
const buttons = document.querySelectorAll(".btn");
if (bar && pctEl) {
const R = 52;
const CIRC = 2 * Math.PI * R; // 円周
bar.style.strokeDasharray = CIRC.toFixed(1);
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let countTimer = null;
// 進捗(0-100)を反映。リングのoffsetと数字を同期
const setProgress = (target) => {
const value = Math.max(0, Math.min(100, target));
// リング: 100%でoffset=0
bar.style.strokeDashoffset = (CIRC * (1 - value / 100)).toFixed(1);
// 数値カウントアップ
if (countTimer) cancelAnimationFrame(countTimer);
const from = parseInt(pctEl.textContent, 10) || 0;
if (reduce) { pctEl.textContent = value; return; }
const start = performance.now();
const DUR = 1000;
const step = (now) => {
const t = Math.min((now - start) / DUR, 1);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
pctEl.textContent = Math.round(from + (value - from) * eased);
if (t < 1) countTimer = requestAnimationFrame(step);
};
countTimer = requestAnimationFrame(step);
};
buttons.forEach((b) => {
b.addEventListener("click", () => setProgress(Number(b.dataset.v)));
});
// 初期演出: 少し待ってから68%へ
setTimeout(() => setProgress(68), 350);
}
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。