アニメ円形ゲージ
SVG円弧のstroke-dashoffsetと数値カウントアップを連動させた円形ゲージ。スコアやパフォーマンス指標の表示に使えます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:KPIモニタ。月間目標の達成率を円形ゲージで表示 -->
<section class="fg-stage">
<header class="fg-head">
<div class="fg-brand"><span class="fg-mark">◆</span> FlowDesk</div>
<span class="fg-period">2026年6月 ・ チーム全体</span>
</header>
<div class="fg-main">
<!-- 円形ゲージ(SVG円弧+数値カウントアップ) -->
<div class="fg-gauge">
<svg viewBox="0 0 200 200" class="fg-svg" role="img" aria-label="目標達成率ゲージ">
<circle class="fg-track" cx="100" cy="100" r="80" />
<circle id="fgGaugeArc" class="fg-arc" cx="100" cy="100" r="80" transform="rotate(-90 100 100)" />
</svg>
<div class="fg-readout">
<span class="fg-num"><span id="fgGaugeNum">0</span><i>%</i></span>
<span class="fg-tag" id="fgGaugeTag">計測中…</span>
</div>
</div>
<div class="fg-meta">
<p class="fg-meta__label">月間目標 達成率</p>
<ul class="fg-stats">
<li><span class="fg-stats__num">¥4.2M</span><span class="fg-stats__lab">実績</span></li>
<li><span class="fg-stats__num">¥5.0M</span><span class="fg-stats__lab">目標</span></li>
<li><span class="fg-stats__num">残12日</span><span class="fg-stats__lab">期間</span></li>
</ul>
<button class="fg-btn" id="fgGaugeReplay" type="button">⟳ 再計測</button>
</div>
</div>
</section>
CSS
/* FlowDesk:KPI達成率(円形ゲージが主役) */
:root {
--navy: #0f1b34;
--blue: #4f7cff;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background: radial-gradient(120% 120% at 50% -10%, #1a2c50 0%, #0f1b34 60%, #0a1228 100%);
color: #eef2ff;
}
.fg-stage { height: 400px; padding: 18px 22px; display: flex; flex-direction: column; }
.fg-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.fg-brand { font-size: 15px; font-weight: 800; letter-spacing: 0.04em; }
.fg-mark { color: var(--blue); }
.fg-period { font-size: 11px; color: rgba(255,255,255,0.55); }
.fg-main { flex: 1; display: flex; align-items: center; gap: 26px; }
/* ゲージ */
.fg-gauge { position: relative; flex: 0 0 auto; width: 176px; height: 176px; }
.fg-svg { width: 176px; height: 176px; }
.fg-track { fill: none; stroke: rgba(255,255,255,0.1); stroke-width: 14; }
.fg-arc {
fill: none; stroke: var(--blue); stroke-width: 14; stroke-linecap: round;
filter: drop-shadow(0 0 8px rgba(79,124,255,0.6));
}
.fg-readout {
position: absolute; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.fg-num { font-size: 42px; font-weight: 800; line-height: 1; }
.fg-num i { font-size: 20px; font-style: normal; color: #9db4ff; margin-left: 2px; }
.fg-tag { margin-top: 6px; font-size: 13px; font-weight: 700; color: #9db4ff; }
/* 右側メタ情報 */
.fg-meta { flex: 1; }
.fg-meta__label { margin: 0 0 12px; font-size: 13px; letter-spacing: 0.06em; color: #9db4ff; }
.fg-stats { list-style: none; margin: 0 0 16px; padding: 0; display: flex; gap: 8px; }
.fg-stats li {
flex: 1; display: flex; flex-direction: column; gap: 3px;
padding: 9px 8px; border-radius: 10px;
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.09);
}
.fg-stats__num { font-size: 15px; font-weight: 800; }
.fg-stats__lab { font-size: 10px; color: rgba(255,255,255,0.55); }
.fg-btn {
font: inherit; font-size: 12px; font-weight: 700;
padding: 10px 18px; border: none; border-radius: 10px; cursor: pointer;
color: #fff; background: linear-gradient(135deg, #5f8bff, var(--blue));
box-shadow: 0 8px 18px rgba(79,124,255,0.4);
transition: transform 0.1s ease, box-shadow 0.2s ease;
}
.fg-btn:hover { box-shadow: 0 12px 24px rgba(79,124,255,0.55); }
.fg-btn:active { transform: scale(0.98); }
JavaScript
// FlowDesk:KPI達成率を円形ゲージで表示(stroke-dashoffset+数値カウントアップ)
(() => {
const arc = document.getElementById('fgGaugeArc');
const num = document.getElementById('fgGaugeNum');
const tag = document.getElementById('fgGaugeTag');
const replay = document.getElementById('fgGaugeReplay');
if (!arc || !num || !tag) return; // null安全
const R = 80;
const CIRC = 2 * Math.PI * R; // 円周長
arc.style.strokeDasharray = String(CIRC);
arc.style.strokeDashoffset = String(CIRC); // 0%から開始
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// 達成率に応じた評価ラベル
function gradeOf(v) {
if (v >= 90) return { text: '目標達成ペース', color: '#6fe0a8' };
if (v >= 70) return { text: '順調', color: '#7dd3fc' };
if (v >= 50) return { text: 'やや遅れ', color: '#fbbf24' };
return { text: '要テコ入れ', color: '#fb7185' };
}
function render(value) {
const v = Math.round(value);
num.textContent = String(v);
const offset = CIRC * (1 - value / 100);
arc.style.strokeDashoffset = String(offset);
}
let rafId = 0;
function animateTo(target) {
cancelAnimationFrame(rafId);
tag.textContent = '計測中…';
tag.style.color = '#9db4ff';
if (reduceMotion) {
render(target);
const g = gradeOf(target);
tag.textContent = g.text;
tag.style.color = g.color;
return;
}
const start = performance.now();
const duration = 1400;
function tick(now) {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
render(target * eased);
if (t < 1) {
rafId = requestAnimationFrame(tick);
} else {
const g = gradeOf(target);
tag.textContent = g.text;
tag.style.color = g.color;
}
}
rafId = requestAnimationFrame(tick);
}
// 62〜96のランダム達成率で計測を演出
function run() {
const target = 62 + Math.floor(Math.random() * 35);
animateTo(target);
}
if (replay) replay.addEventListener('click', run);
run();
})();
コード
HTML
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">パフォーマンススコア</h2>
<p class="dv-sub">SVGストロークで描く円形ゲージ(カウントアップ連動)</p>
</figcaption>
<div class="dv-gauge">
<svg viewBox="0 0 200 200" class="dv-gauge__svg" role="img" aria-label="スコアを示す円形ゲージ">
<defs>
<linearGradient id="gaugeGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#34d399" />
<stop offset="55%" stop-color="#22d3ee" />
<stop offset="100%" stop-color="#818cf8" />
</linearGradient>
</defs>
<!-- 背景トラック -->
<circle class="dv-gauge__track" cx="100" cy="100" r="80" />
<!-- 進捗アーク(JSでstroke-dashoffsetを制御) -->
<circle id="gaugeArc" class="dv-gauge__arc" cx="100" cy="100" r="80" />
</svg>
<div class="dv-gauge__label">
<span id="gaugeNum" class="dv-gauge__num">0</span>
<span class="dv-gauge__unit">/ 100</span>
<span id="gaugeTag" class="dv-gauge__tag">計測中…</span>
</div>
</div>
<button id="gaugeReplay" class="dv-btn" type="button">再計測</button>
</figure>
</div>
CSS
:root {
--dv-radius: 18px;
--dv-ink: #ecfeff;
--dv-sub: #a5b4fc;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--dv-ink);
background:
radial-gradient(700px 420px at 80% -10%, #155e75 0%, transparent 55%),
linear-gradient(160deg, #0f172a, #020617);
}
.dv-wrap { width: min(92vw, 560px); padding: 20px; }
.dv-card {
margin: 0;
padding: 18px 24px 18px;
border-radius: var(--dv-radius);
background: rgba(15, 23, 42, 0.55);
border: 1px solid rgba(129, 140, 248, 0.2);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
text-align: center;
}
.dv-head { margin-bottom: 8px; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }
.dv-gauge {
position: relative;
width: 168px;
height: 168px;
margin: 6px auto 4px;
}
.dv-gauge__svg {
width: 100%;
height: 100%;
/* 12時方向を始点にするため反時計回り90度回転 */
transform: rotate(-90deg);
}
.dv-gauge__track {
fill: none;
stroke: rgba(148, 163, 184, 0.18);
stroke-width: 16;
}
.dv-gauge__arc {
fill: none;
stroke: url(#gaugeGrad);
stroke-width: 16;
stroke-linecap: round;
filter: drop-shadow(0 0 8px rgba(34, 211, 238, 0.55));
}
.dv-gauge__label {
position: absolute;
inset: 0;
display: grid;
place-content: center;
gap: 0;
line-height: 1;
}
.dv-gauge__num {
font-size: 40px;
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.dv-gauge__unit { font-size: 14px; color: var(--dv-sub); margin-top: 4px; }
.dv-gauge__tag { font-size: 12px; color: #67e8f9; margin-top: 8px; font-weight: 600; }
.dv-btn {
margin-top: 14px;
padding: 9px 20px;
border: 1px solid rgba(129, 140, 248, 0.4);
border-radius: 999px;
background: rgba(129, 140, 248, 0.12);
color: var(--dv-ink);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background .2s ease, transform .1s ease;
}
.dv-btn:hover { background: rgba(129, 140, 248, 0.25); }
.dv-btn:active { transform: scale(0.96); }
.dv-btn:focus-visible { outline: 2px solid #67e8f9; outline-offset: 2px; }
JavaScript
// SVG円弧のstroke-dashoffsetと数値カウントアップを連動させた円形ゲージ
(() => {
const arc = document.getElementById('gaugeArc');
const num = document.getElementById('gaugeNum');
const tag = document.getElementById('gaugeTag');
const replay = document.getElementById('gaugeReplay');
if (!arc || !num || !tag) return; // null安全
const R = 80;
const CIRC = 2 * Math.PI * R; // 円周長
arc.style.strokeDasharray = String(CIRC);
arc.style.strokeDashoffset = String(CIRC); // 0%から開始
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// スコアに応じた評価ラベル
function gradeOf(v) {
if (v >= 90) return { text: '優秀', color: '#34d399' };
if (v >= 70) return { text: '良好', color: '#22d3ee' };
if (v >= 50) return { text: '普通', color: '#818cf8' };
return { text: '要改善', color: '#fb7185' };
}
function render(value) {
const v = Math.round(value);
num.textContent = String(v);
const offset = CIRC * (1 - value / 100);
arc.style.strokeDashoffset = String(offset);
}
let rafId = 0;
function animateTo(target) {
cancelAnimationFrame(rafId);
tag.textContent = '計測中…';
if (reduceMotion) {
render(target);
const g = gradeOf(target);
tag.textContent = g.text;
tag.style.color = g.color;
return;
}
const start = performance.now();
const duration = 1400;
function tick(now) {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
render(target * eased);
if (t < 1) {
rafId = requestAnimationFrame(tick);
} else {
const g = gradeOf(target);
tag.textContent = g.text;
tag.style.color = g.color;
}
}
rafId = requestAnimationFrame(tick);
}
// 50〜98のランダム値で計測を演出
function run() {
const target = 50 + Math.floor(Math.random() * 49);
animateTo(target);
}
if (replay) replay.addEventListener('click', run);
run();
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「アニメ円形ゲージ」の効果を追加してください。
# 追加してほしい効果
アニメ円形ゲージ(データ可視化)
SVG円弧のstroke-dashoffsetと数値カウントアップを連動させた円形ゲージ。スコアやパフォーマンス指標の表示に使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">パフォーマンススコア</h2>
<p class="dv-sub">SVGストロークで描く円形ゲージ(カウントアップ連動)</p>
</figcaption>
<div class="dv-gauge">
<svg viewBox="0 0 200 200" class="dv-gauge__svg" role="img" aria-label="スコアを示す円形ゲージ">
<defs>
<linearGradient id="gaugeGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#34d399" />
<stop offset="55%" stop-color="#22d3ee" />
<stop offset="100%" stop-color="#818cf8" />
</linearGradient>
</defs>
<!-- 背景トラック -->
<circle class="dv-gauge__track" cx="100" cy="100" r="80" />
<!-- 進捗アーク(JSでstroke-dashoffsetを制御) -->
<circle id="gaugeArc" class="dv-gauge__arc" cx="100" cy="100" r="80" />
</svg>
<div class="dv-gauge__label">
<span id="gaugeNum" class="dv-gauge__num">0</span>
<span class="dv-gauge__unit">/ 100</span>
<span id="gaugeTag" class="dv-gauge__tag">計測中…</span>
</div>
</div>
<button id="gaugeReplay" class="dv-btn" type="button">再計測</button>
</figure>
</div>
【CSS】
:root {
--dv-radius: 18px;
--dv-ink: #ecfeff;
--dv-sub: #a5b4fc;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--dv-ink);
background:
radial-gradient(700px 420px at 80% -10%, #155e75 0%, transparent 55%),
linear-gradient(160deg, #0f172a, #020617);
}
.dv-wrap { width: min(92vw, 560px); padding: 20px; }
.dv-card {
margin: 0;
padding: 18px 24px 18px;
border-radius: var(--dv-radius);
background: rgba(15, 23, 42, 0.55);
border: 1px solid rgba(129, 140, 248, 0.2);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.7);
backdrop-filter: blur(6px);
text-align: center;
}
.dv-head { margin-bottom: 8px; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }
.dv-gauge {
position: relative;
width: 168px;
height: 168px;
margin: 6px auto 4px;
}
.dv-gauge__svg {
width: 100%;
height: 100%;
/* 12時方向を始点にするため反時計回り90度回転 */
transform: rotate(-90deg);
}
.dv-gauge__track {
fill: none;
stroke: rgba(148, 163, 184, 0.18);
stroke-width: 16;
}
.dv-gauge__arc {
fill: none;
stroke: url(#gaugeGrad);
stroke-width: 16;
stroke-linecap: round;
filter: drop-shadow(0 0 8px rgba(34, 211, 238, 0.55));
}
.dv-gauge__label {
position: absolute;
inset: 0;
display: grid;
place-content: center;
gap: 0;
line-height: 1;
}
.dv-gauge__num {
font-size: 40px;
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.dv-gauge__unit { font-size: 14px; color: var(--dv-sub); margin-top: 4px; }
.dv-gauge__tag { font-size: 12px; color: #67e8f9; margin-top: 8px; font-weight: 600; }
.dv-btn {
margin-top: 14px;
padding: 9px 20px;
border: 1px solid rgba(129, 140, 248, 0.4);
border-radius: 999px;
background: rgba(129, 140, 248, 0.12);
color: var(--dv-ink);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background .2s ease, transform .1s ease;
}
.dv-btn:hover { background: rgba(129, 140, 248, 0.25); }
.dv-btn:active { transform: scale(0.96); }
.dv-btn:focus-visible { outline: 2px solid #67e8f9; outline-offset: 2px; }
【JavaScript】
// SVG円弧のstroke-dashoffsetと数値カウントアップを連動させた円形ゲージ
(() => {
const arc = document.getElementById('gaugeArc');
const num = document.getElementById('gaugeNum');
const tag = document.getElementById('gaugeTag');
const replay = document.getElementById('gaugeReplay');
if (!arc || !num || !tag) return; // null安全
const R = 80;
const CIRC = 2 * Math.PI * R; // 円周長
arc.style.strokeDasharray = String(CIRC);
arc.style.strokeDashoffset = String(CIRC); // 0%から開始
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// スコアに応じた評価ラベル
function gradeOf(v) {
if (v >= 90) return { text: '優秀', color: '#34d399' };
if (v >= 70) return { text: '良好', color: '#22d3ee' };
if (v >= 50) return { text: '普通', color: '#818cf8' };
return { text: '要改善', color: '#fb7185' };
}
function render(value) {
const v = Math.round(value);
num.textContent = String(v);
const offset = CIRC * (1 - value / 100);
arc.style.strokeDashoffset = String(offset);
}
let rafId = 0;
function animateTo(target) {
cancelAnimationFrame(rafId);
tag.textContent = '計測中…';
if (reduceMotion) {
render(target);
const g = gradeOf(target);
tag.textContent = g.text;
tag.style.color = g.color;
return;
}
const start = performance.now();
const duration = 1400;
function tick(now) {
const t = Math.min(1, (now - start) / duration);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
render(target * eased);
if (t < 1) {
rafId = requestAnimationFrame(tick);
} else {
const g = gradeOf(target);
tag.textContent = g.text;
tag.style.color = g.color;
}
}
rafId = requestAnimationFrame(tick);
}
// 50〜98のランダム値で計測を演出
function run() {
const target = 50 + Math.floor(Math.random() * 49);
animateTo(target);
}
if (replay) replay.addEventListener('click', run);
run();
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。