円形プログレスゲージ

円のstroke-dashoffsetで進捗を可視化し、中央の数値をカウントアップ。ダッシュボードやスキル表示に使えます。

#svg#js#animation#ui

ライブデモ

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