進捗ゲージ(円形プログレス)
このコンポーネントについて
進捗ゲージ(円形プログレス)は、SVGで描いた円弧を使って割合や達成度を視覚的に伝えるUIです。直線的なプログレスバーに比べてコンパクトに収まるため、ダッシュボードのKPIカードやプロフィール設定画面など「ひと目で状態を把握させたい」場面で重宝します(circular progress / progress gauge とも呼ばれます)。
このページではオンボーディング画面の定番である「プロフィール入力完了率」を題材に、SVGの<circle>要素とstroke-dasharray / stroke-dashoffsetで弧を描画し、ページ読み込み時に0%から目標値までアニメーションする実装を紹介します。中央には完了率(パーセント)と入力済み項目数をあわせて表示し、割合と実数の両方を直感的に伝えます。
- SVGによる円弧描画 — 背景トラックと進捗の弧を二重の
<circle>で重ね、stroke-dasharray/stroke-dashoffsetで割合を表現する - 読み込み時のアニメーション — ページ表示時に弧が0%から目標値まで滑らかに伸び、中央のパーセント数値もカウントアップ表示される
- 青系グラデーション — 進捗の弧にサイトのアクセントカラーを基調とした濃淡グラデーション(
<linearGradient>)を適用し、単色のプログレスバーとの差別化を図る - 定量的な状態表示 — 中央に「80%」と「8 / 10 項目 入力済み」を併記し、割合と実数の両方を一目で把握できる
- デモ用スライダーで連動更新 — スライダーを操作すると弧・パーセント・項目数表示がリアルタイムに連動して変化する
実装のポイント・注意点
SVGで円弧の進捗を表現するには、<circle>のstroke-dasharrayに円周の長さ(2 * Math.PI * r)を設定し、stroke-dashoffsetを「円周 −(円周 × 割合)」で計算して引くことで、指定割合分だけ弧を見せる仕組みを使います。stroke-dashoffsetの変化にtransitionを設定すればなめらかなアニメーションになります。
グラデーションは<defs><linearGradient>をSVG内に定義し、進捗の弧のstroke属性にurl(#gradientId)で参照します。SVG全体をrotate(-90deg)で回転させると、弧が12時の位置から時計回りに伸びるようになり、一般的な円形プログレスの見え方になります。
中央のパーセント数値のカウントアップは、requestAnimationFrameで目標値まで徐々に値を増やしながらtextContentを更新する方法がシンプルです。スマートフォンで表示が崩れないよう、ゲージのサイズは固定pxではなく親要素に対して中央寄せで配置しています。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
プロフィール入力完了率
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>進捗ゲージ サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="cg-wrap">
<div class="cg-card">
<div class="cg-gauge">
<svg class="cg-svg" width="180" height="180" viewBox="0 0 180 180" aria-hidden="true">
<defs>
<linearGradient id="cgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#6EA8F7"/>
<stop offset="100%" stop-color="#1554A0"/>
</linearGradient>
</defs>
<circle class="cg-track" cx="90" cy="90" r="78"/>
<circle class="cg-bar" id="cgBar" cx="90" cy="90" r="78"/>
</svg>
<div class="cg-center" id="cgCenter" role="meter" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100" aria-label="プロフィール入力完了率 0%">
<span class="cg-percent" id="cgPercent">0%</span>
<span class="cg-sublabel" id="cgSublabel">0 / 10 項目 入力済み</span>
</div>
</div>
<p class="cg-title">プロフィール入力完了率</p>
</div>
<!-- デモ用スライダー(不要な場合は削除してください)-->
<div class="cg-slider-wrap">
<label class="cg-slider-label" for="cgSlider">
入力済み項目数を変えてみる:<span id="cgSliderVal">8</span> / 10
</label>
<input type="range" id="cgSlider" class="cg-slider" min="0" max="10" step="1" value="8">
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
/* ===== CSS変数(色の調整はここで)===== */
:root {
--cg-color-start: #6EA8F7; /* グラデーション開始色(薄め) */
--cg-color-end: #1554A0; /* グラデーション終了色(濃いめ) */
--cg-track-bg: #E5E7EB;
--cg-text: #1A2332;
--cg-text-muted: #5A6A7A;
}
*, *::before, *::after { box-sizing: border-box; }
body { font-family: sans-serif; padding: 32px 24px; background: #F4F6F9; }
/* ===== カード ===== */
.cg-card {
background: #fff;
border-radius: 10px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid #E8EDF2;
max-width: 280px;
margin: 0 auto;
text-align: center;
}
/* ===== ゲージ(SVG+中央テキストの基準コンテナ)===== */
.cg-gauge {
position: relative;
width: 180px;
height: 180px;
margin: 0 auto;
}
/* 12時の位置から時計回りに弧が伸びるよう90度回転させる */
.cg-svg {
transform: rotate(-90deg);
}
.cg-track {
fill: none;
stroke: var(--cg-track-bg);
stroke-width: 14;
}
.cg-bar {
fill: none;
stroke: url(#cgGradient);
stroke-width: 14;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
}
/* SVGの中央にパーセント表示を重ねる */
.cg-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.cg-percent {
font-size: 32px;
font-weight: 700;
color: var(--cg-text);
}
.cg-sublabel {
font-size: 12px;
color: var(--cg-text-muted);
margin-top: 4px;
}
.cg-title {
margin: 12px 0 0;
font-size: 14px;
color: var(--cg-text-muted);
}
/* ===== デモ用スライダー ===== */
.cg-slider-wrap {
margin-top: 20px;
max-width: 280px;
margin-left: auto;
margin-right: auto;
}
.cg-slider-label {
display: block;
font-size: 13px;
color: var(--cg-text-muted);
margin-bottom: 8px;
}
.cg-slider {
width: 100%;
cursor: pointer;
}
var TOTAL_ITEMS = 10; // プロフィール入力項目の総数
var INIT_ITEMS = 8; // 初期の入力済み項目数(80%)
var RADIUS = 78; // SVG circle の半径(r属性と合わせる)
var CIRCUMFERENCE = 2 * Math.PI * RADIUS; // 円周
var barEl = document.getElementById('cgBar');
var centerEl = document.getElementById('cgCenter');
var percentEl = document.getElementById('cgPercent');
var sublabelEl = document.getElementById('cgSublabel');
var sliderEl = document.getElementById('cgSlider');
var sliderValEl = document.getElementById('cgSliderVal');
var animationFrameId = null; // 実行中のアニメーションを中断できるように保持
// 円弧の見える長さを割合に応じて設定する
function setProgress(percent) {
var offset = CIRCUMFERENCE - (CIRCUMFERENCE * percent / 100);
barEl.style.strokeDasharray = CIRCUMFERENCE;
barEl.style.strokeDashoffset = offset;
}
// 中央のパーセント・実数表示とアクセシビリティ属性を更新する
function updateLabels(items, percent) {
percentEl.textContent = percent + '%';
sublabelEl.textContent = items + ' / ' + TOTAL_ITEMS + ' 項目 入力済み';
centerEl.setAttribute('aria-valuenow', percent);
centerEl.setAttribute('aria-label', 'プロフィール入力完了率 ' + percent + '%');
}
// 0 から targetItems まで弧と数値を同時にカウントアップさせる
function animateTo(targetItems) {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
}
var duration = 800; // アニメーション時間(ミリ秒)
var startTime = null;
function step(timestamp) {
if (startTime === null) startTime = timestamp;
var progress = Math.min((timestamp - startTime) / duration, 1);
var currentItems = Math.round(targetItems * progress);
var currentPercent = Math.round(currentItems / TOTAL_ITEMS * 100);
setProgress(currentPercent);
updateLabels(currentItems, currentPercent);
if (progress < 1) {
animationFrameId = requestAnimationFrame(step);
} else {
animationFrameId = null;
}
}
animationFrameId = requestAnimationFrame(step);
}
// スライダー操作はアニメーションさせず即座に反映する
sliderEl.addEventListener('input', function() {
var items = parseInt(this.value, 10);
var percent = Math.round(items / TOTAL_ITEMS * 100);
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
sliderValEl.textContent = items;
setProgress(percent);
updateLabels(items, percent);
});
// 初期状態に戻す(リセットボタンからも呼ばれる)
function resetDemo() {
sliderEl.value = INIT_ITEMS;
sliderValEl.textContent = INIT_ITEMS;
setProgress(0);
updateLabels(0, 0);
animateTo(INIT_ITEMS);
}
// 初期化(ページ読み込み時に0%→目標値までアニメーション)
resetDemo();
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や列数変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# 進捗ゲージ(円形プログレス) 作成依頼
## 概要
SVGで円弧を描画し、割合をビジュアルに表示する円形プログレスゲージ(circular progress gauge)を作成してください。
題材は「プロフィール入力完了率」とし、ページ読み込み時に弧が0%から目標値までアニメーションし、
デモ用スライダーで値をリアルタイムに変更できるようにしてください。
## 要件
- SVGの`<circle>`を二重に重ね、背景トラックと進捗の弧を表現する
- 進捗の弧には青系のグラデーション(`<linearGradient>`)を適用する
- 中央に完了率(パーセント)と「n / 10 項目 入力済み」の実数表示を併記する
- ページ読み込み時に弧が0%から目標値まで伸びながら、中央のパーセント数値もカウントアップ表示する
- デモ用スライダー(0〜10)で入力済み項目数を変更でき、弧・パーセント・実数表示がリアルタイムに連動する
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
## 動作詳細
- 全項目数は10、初期の入力済み項目数は8(80%)
- 円弧の進捗は `stroke-dasharray` に円周(`2 * Math.PI * 半径`)を設定し、`stroke-dashoffset` を「円周 −(円周 × 割合)」で計算して制御する
- SVG全体を `rotate(-90deg)` で回転させ、弧が12時の位置から時計回りに伸びるようにする
- 初回表示時は `requestAnimationFrame` などで0から目標値までカウントアップしながら弧と数値を同時に更新する
- スライダー操作時はアニメーションさせず、即座に値を反映する
- アクセシビリティ対応:ゲージのコンテナに `role="meter"`・`aria-valuenow`・`aria-valuemin`・`aria-valuemax`・`aria-label` を設定し、値の変化に合わせて更新する
- テキストの動的更新は `textContent` のみ使用すること(`innerHTML` に変数を結合しない)
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。