プログレスバー 1 — APIポーリング+フェイクプログレス
このコンポーネントについて
プログレスバーは、時間のかかる処理の進捗を視覚的に伝えるUIコンポーネントです。「どのくらい終わったか」が見えることで、ユーザーの不安や離脱を防ぎます。
このページでは、バックエンド処理の進捗をAPIで定期的に取得して更新する「APIポーリング」パターンと、進捗が取得できない場面で擬似的にバーを進める「フェイクプログレス」パターンの2種類を紹介します。動画変換・ファイル処理・データ集計など、完了まで時間がかかる処理のUI実装に役立ちます。
- APIポーリング —
setIntervalで定期的にAPIを呼び出し、返ってきた進捗率でバーをリアルタイム更新する。ステータステキストで処理フェーズもあわせて表示 - フェイクプログレス — 進捗が取得できない場面で、進むほど遅くなるアルゴリズムでバーを自動前進させ、完了通知を受け取ったら一気に100%にジャンプさせる
実装のポイント・注意点
APIポーリングで最も重要なのは、完了時・リセット時に必ず clearInterval を呼ぶことです。呼び忘れると処理が終わっているのにリクエストが飛び続け、予期しない動作やリソース消費につながります。実装では intervalId を変数で保持し、必要なタイミングで確実に止めてください。
フェイクプログレスは progress += (90 - progress) * rate という式で動きます。現在の進捗が90%に近づくほど増分が小さくなり、自然に失速します。rate は 0.02〜0.05 程度が視覚的に自然です。このパターンはSaaSのインポート機能や処理待ち画面でよく使われる実装テクニックで、「止まってる感をなくすためのUX的工夫」です。実際の進捗を反映していない点を理解した上で使いましょう。
2つのパターンは独立して動作するよう、pollIntervalId・fakeRafId などの状態変数はパターンごとに別々に管理してください。
デモ
Pattern 1 — APIポーリング
待機中
Pattern 2 — フェイクプログレス
待機中
90%に達したら「処理完了」ボタンを押すと100%になります。
サンプルソース
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>
<!-- ========================================
プログレスバー — APIポーリング
setInterval でAPIレスポンスをシミュレートし
返ってきた進捗率でバーを更新する
======================================== -->
<div class="pb-wrap">
<h2 class="pb-title">Pattern 1 — APIポーリング</h2>
<div class="pb-bar-wrap">
<div class="pb-bar" id="poll-bar"></div>
</div>
<p class="pb-status" id="poll-status">待機中</p>
<div class="pb-controls">
<button id="poll-start-btn" type="button" onclick="startPolling()">処理を開始</button>
<button id="poll-reset-btn" type="button" onclick="resetPolling()" hidden>リセット</button>
</div>
</div>
<!-- ========================================
プログレスバー — フェイクプログレス
処理完了が通知されるまで擬似的にバーを進め
完了時に100%にジャンプさせる
======================================== -->
<div class="pb-wrap">
<h2 class="pb-title">Pattern 2 — フェイクプログレス</h2>
<div class="pb-bar-wrap">
<div class="pb-bar" id="fake-bar"></div>
</div>
<p class="pb-status" id="fake-status">待機中</p>
<div class="pb-controls">
<button id="fake-start-btn" type="button" onclick="startFake()">処理を開始</button>
<button id="fake-done-btn" type="button" onclick="completeFake()" disabled>処理完了</button>
<button id="fake-reset-btn" type="button" onclick="resetFake()" hidden>リセット</button>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
/* プログレスバー パターン集 — style.css
色を変えたいときは :root の変数を書き換えるだけでOK */
:root {
--bar-color: #2563EB; /* バー通常色(青) */
--bar-done: #16A34A; /* バー完了色(緑) */
--bar-bg: #E5E7EB; /* バー外枠の背景 */
--text-muted: #5A6A7A; /* サブテキスト色 */
}
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 24px;
max-width: 520px;
margin: 0 auto;
background: #fff;
color: #1A2332;
}
/* プログレスバーを囲むブロック */
.pb-wrap {
margin-bottom: 40px;
padding: 20px;
background: #F4F6F9;
border-radius: 8px;
}
.pb-title {
margin: 0 0 16px;
font-size: 14px;
font-weight: 700;
color: #2563EB;
}
/* バー外枠 */
.pb-bar-wrap {
background: var(--bar-bg);
border-radius: 9999px;
height: 14px;
overflow: hidden;
margin-bottom: 10px;
}
/* バー本体 */
.pb-bar {
height: 100%;
width: 0%;
background: var(--bar-color);
border-radius: 9999px;
transition: width 0.4s ease;
}
/* 完了時に緑に切り替える */
.pb-bar.done { background: var(--bar-done); }
/* ステータステキスト */
.pb-status {
font-size: 13px;
color: var(--text-muted);
margin: 0 0 16px;
min-height: 1.5em;
}
/* ボタン群 */
.pb-controls { display: flex; gap: 8px; flex-wrap: wrap; }
.pb-controls button {
padding: 8px 18px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s, opacity 0.15s;
}
/* 開始ボタン(青) */
#poll-start-btn, #fake-start-btn {
background: #2563EB;
color: #fff;
border: none;
}
#poll-start-btn:hover:not(:disabled),
#fake-start-btn:hover:not(:disabled) { background: #1D4ED8; }
/* 完了ボタン(緑) */
#fake-done-btn {
background: #16A34A;
color: #fff;
border: none;
}
#fake-done-btn:hover:not(:disabled) { background: #15803D; }
/* リセットボタン */
#poll-reset-btn, #fake-reset-btn {
background: #fff;
color: #5A6A7A;
border: 1.5px solid #D0D7E0;
}
#poll-reset-btn:hover, #fake-reset-btn:hover { background: #F4F6F9; }
button:disabled { opacity: 0.4; cursor: not-allowed; }
@media (max-width: 480px) { body { padding: 16px; } }
// ==========================================
// Pattern 1 — APIポーリング
// ==========================================
var pollIntervalId = null;
var pollProgress = 0;
var POLL_INTERVAL = 1500; // ポーリング間隔(ミリ秒)
// 進捗率に応じたステータステキストを返す
function getPollStatus(p) {
if (p >= 100) return '完了!';
if (p >= 80) return '最終処理中... (' + p + '%)';
if (p >= 40) return '処理中... (' + p + '%)';
if (p >= 10) return 'データ取得中... (' + p + '%)';
return '接続中...';
}
function startPolling() {
if (pollIntervalId) return; // 二重起動を防ぐ
document.getElementById('poll-start-btn').disabled = true;
// ── Step 1: 処理を開始するAPIを叩いてジョブIDを受け取る ──
// 実際の実装:
// const res = await fetch('/jobs', { method: 'POST' });
// const { jobId } = await res.json(); // → { jobId: 'abc123' }
// デモ用(サーバーなしで動かすため即座に仮IDを生成):
var jobId = 'demo-' + Date.now();
document.getElementById('poll-status').textContent = '接続中...';
// ── Step 2: 一定間隔でAPIを叩いて進捗を確認する(ポーリング)──
// 実際の実装:
// const res = await fetch('/jobs/' + jobId + '/status');
// const { progress } = await res.json(); // → { progress: 68 } のような数値
// pollProgress = progress;
// デモ用(5〜15%のランダム増分でAPIレスポンスをシミュレート):
pollIntervalId = setInterval(function () {
var increment = Math.floor(Math.random() * 11) + 5;
pollProgress = Math.min(pollProgress + increment, 100);
var bar = document.getElementById('poll-bar');
var status = document.getElementById('poll-status');
bar.style.width = pollProgress + '%';
status.textContent = getPollStatus(pollProgress);
if (pollProgress >= 100) {
bar.classList.add('done');
clearInterval(pollIntervalId); // 完了したらポーリングを止める
pollIntervalId = null;
document.getElementById('poll-reset-btn').hidden = false;
}
}, POLL_INTERVAL);
}
function resetPolling() {
// 実行中のインターバルを必ず止める
if (pollIntervalId) { clearInterval(pollIntervalId); pollIntervalId = null; }
pollProgress = 0;
var bar = document.getElementById('poll-bar');
bar.style.width = '0%';
bar.classList.remove('done');
document.getElementById('poll-status').textContent = '待機中';
document.getElementById('poll-start-btn').disabled = false;
document.getElementById('poll-reset-btn').hidden = true;
}
// ==========================================
// Pattern 2 — フェイクプログレス
// ==========================================
var fakeProgress = 0;
var fakeRafId = null;
var FAKE_CAP = 90; // 自動前進の上限(%)
var FAKE_RATE = 0.03; // 減速係数(小さいほどゆっくり進む)
function fakeTick() {
// 90%に近づくほど増分が小さくなる(指数的減速)
fakeProgress += (FAKE_CAP - fakeProgress) * FAKE_RATE;
var bar = document.getElementById('fake-bar');
var status = document.getElementById('fake-status');
bar.style.width = Math.floor(fakeProgress) + '%';
status.textContent = '処理中... (' + Math.floor(fakeProgress) + '%)';
if (fakeProgress < FAKE_CAP - 0.1) {
fakeRafId = requestAnimationFrame(fakeTick);
} else {
// 90%付近で停止し「処理完了」ボタンを活性化する
fakeProgress = FAKE_CAP;
bar.style.width = FAKE_CAP + '%';
status.textContent = '処理中... しばらくお待ちください';
document.getElementById('fake-done-btn').disabled = false;
}
}
function startFake() {
if (fakeRafId) return; // 二重起動を防ぐ
document.getElementById('fake-start-btn').disabled = true;
fakeRafId = requestAnimationFrame(fakeTick);
}
function completeFake() {
fakeProgress = 100;
var bar = document.getElementById('fake-bar');
bar.style.width = '100%';
bar.classList.add('done');
document.getElementById('fake-status').textContent = '完了!';
document.getElementById('fake-done-btn').disabled = true;
document.getElementById('fake-reset-btn').hidden = false;
}
function resetFake() {
if (fakeRafId) { cancelAnimationFrame(fakeRafId); fakeRafId = null; }
fakeProgress = 0;
var bar = document.getElementById('fake-bar');
bar.style.width = '0%';
bar.classList.remove('done');
document.getElementById('fake-status').textContent = '待機中';
document.getElementById('fake-start-btn').disabled = false;
document.getElementById('fake-done-btn').disabled = true;
document.getElementById('fake-reset-btn').hidden = true;
}
AI用プロンプト
各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。色変更やライブラリ指定など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
Pattern 1 — APIポーリング
# プログレスバー(APIポーリング)作成依頼
## 概要
APIを定期的にポーリングして進捗を取得し、プログレスバーをリアルタイム更新するUIを実装してください。
動画変換・データ集計など、バックエンドで時間がかかる処理の進捗表示を想定しています。
## 要件
- 「処理を開始」ボタンをクリックで処理開始
- プログレスバーに進捗率(0〜100%)をリアルタイム表示
- バーの下にステータステキストを表示(「データ取得中... (34%)」「完了!」など)
- 完了後にリセットボタンを表示して初期状態に戻せる
- 処理中はボタンを非活性化して二重実行を防ぐ
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
## 動作詳細
setInterval で 1〜2秒ごとにAPIレスポンスをシミュレートし、5〜15%のランダムな増分で進捗率を更新する。
100%に達したらインターバルをクリアし、ステータスを「完了!」に変更してリセットボタンを表示する。
実際の実装では fetch('/jobs/:id/status') を叩き、レスポンスの progress 値でバーを更新する。
完了・リセット時は必ず clearInterval を呼んでインターバルを止めること。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。
Pattern 2 — フェイクプログレス
# プログレスバー(フェイクプログレス)作成依頼
## 概要
実際の進捗が取得できない場面で「止まってる感」を解消するフェイクプログレスバーを実装してください。
処理開始で自動的にバーが前進し、処理完了の通知を受け取ったら一気に100%にジャンプします。
## 要件
- 「処理を開始」ボタンで自動的にバーが進み始める
- 進むほど遅くなる(指数的に減速)
- 90%付近で自動停止する
- 「処理完了」ボタンで残りを一気に100%にジャンプさせる
- 完了後にリセットできる
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
## 動作詳細
requestAnimationFrame を使い、progress += (90 - progress) * 0.03 の式で進捗を更新する。
90%で自動停止し「処理完了」ボタンを活性化する。
「処理完了」がクリックされたら progress を 100 にセットしてバーを完了状態(緑)にする。
完了後はリセットボタンを表示して初期状態に戻せるようにする。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。