アニメ円形ゲージ

SVG円弧のstroke-dashoffsetと数値カウントアップを連動させた円形ゲージ。スコアやパフォーマンス指標の表示に使えます。

#svg#gauge#animation#interaction

ライブデモ

使用例(お題: 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。