プログレスバー
不規則に進む疑似アップロードをパーセントとMB表示で見せる進捗バー。グラデーションが流れ完了状態も色で表現します。fetch進捗の可視化に最適。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk データインポート:プログレスバーでアップロード進捗を可視化 -->
<div class="fd-import">
<header class="fd-import__bar">
<span class="fd-import__logo"><span class="fd-import__mark">◆</span> FlowDesk</span>
<span class="fd-import__step">データ移行 3 / 4</span>
</header>
<div class="fd-card">
<div class="fd-file">
<span class="fd-file__ico">▤</span>
<div class="fd-file__meta">
<b class="fd-file__name">team_tasks_2025.csv</b>
<span class="fd-file__size" id="fdSize">0.0 / 18.4 MB</span>
</div>
<span class="fd-file__pct" id="fdPct">0%</span>
</div>
<!-- プログレスバー(主役) -->
<div class="fd-track"><span class="fd-fill" id="fdFill"></span></div>
<p class="fd-status" id="fdStatus">アップロードしています…</p>
</div>
<p class="fd-hint">CSVをインポートしてワークスペースにタスクを取り込みます。</p>
</div>
CSS
/* FlowDesk データインポート:プログレスバー */
:root {
--navy: #0f1b34;
--navy2: #16264a;
--blue: #4f7cff;
--line: rgba(255,255,255,0.08);
--muted: #9fb0d4;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
background: var(--navy);
color: #fff;
overflow: hidden;
}
.fd-import { width: 360px; }
.fd-import__bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.fd-import__logo { font-size: 15px; font-weight: 700; }
.fd-import__mark { color: var(--blue); margin-right: 4px; }
.fd-import__step { font-size: 11px; color: var(--muted); }
.fd-card {
background: var(--navy2);
border: 1px solid var(--line);
border-radius: 16px;
padding: 18px;
}
.fd-file { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.fd-file__ico {
flex: none;
width: 38px; height: 38px;
display: grid;
place-items: center;
border-radius: 10px;
background: rgba(79,124,255,0.16);
color: var(--blue);
font-size: 18px;
}
.fd-file__meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.fd-file__name { font-size: 13px; }
.fd-file__size { font-size: 11px; color: var(--muted); }
.fd-file__pct { font-size: 16px; font-weight: 700; color: var(--blue); }
/* プログレスバー本体 */
.fd-track {
height: 10px;
border-radius: 999px;
background: rgba(255,255,255,0.08);
overflow: hidden;
}
.fd-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
/* 流れるグラデーション */
background: linear-gradient(90deg, #4f7cff, #7da0ff, #4f7cff);
background-size: 200% 100%;
animation: fd-flow 1.4s linear infinite;
transition: width 0.25s ease;
}
.fd-fill.is-done {
background: #4ade80;
animation: none;
}
@keyframes fd-flow { to { background-position: -200% 0; } }
.fd-status { margin: 14px 0 0; font-size: 11.5px; color: var(--muted); }
.fd-status.is-done { color: #4ade80; }
.fd-hint { margin: 14px 2px 0; font-size: 11px; color: var(--muted); line-height: 1.6; }
@media (prefers-reduced-motion: reduce) {
.fd-fill { animation: none; transition: none; }
}
JavaScript
// FlowDesk インポート:不規則に進む疑似アップロード(MB+%、完了で緑・ループ)
const fill = document.getElementById('fdFill');
const pct = document.getElementById('fdPct');
const size = document.getElementById('fdSize');
const status = document.getElementById('fdStatus');
const TOTAL = 18.4; // MB
let p = 0;
let timer = null;
function reset() {
p = 0;
if (fill) { fill.style.width = '0%'; fill.classList.remove('is-done'); }
if (pct) pct.textContent = '0%';
if (size) size.textContent = '0.0 / ' + TOTAL.toFixed(1) + ' MB';
if (status) { status.textContent = 'アップロードしています…'; status.classList.remove('is-done'); }
}
function step() {
// 不規則に進む(残りに応じて加速感を出す)
p = Math.min(p + Math.random() * 9 + 2, 100);
const done = Math.round(p);
if (fill) fill.style.width = done + '%';
if (pct) pct.textContent = done + '%';
if (size) size.textContent = (TOTAL * p / 100).toFixed(1) + ' / ' + TOTAL.toFixed(1) + ' MB';
if (p >= 100) {
clearInterval(timer);
if (fill) fill.classList.add('is-done');
if (status) { status.textContent = 'インポート完了 — タスクを取り込みました'; status.classList.add('is-done'); }
// しばらくして再スタート(ループ)
setTimeout(start, 2600);
}
}
function start() {
reset();
clearInterval(timer);
timer = setInterval(step, 320);
}
// 初回起動
start();
コード
HTML
<!-- プログレスバー: 進捗をパーセント表示・自動で進みグラデが流れる -->
<div class="pb-stage">
<div class="pb-panel">
<div class="pb-head">
<span class="pb-label" id="pbLabel">アップロード中…</span>
<span class="pb-pct" id="pbPct">0%</span>
</div>
<!-- 進捗トラック -->
<div class="pb-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="pbBar">
<div class="pb-fill" id="pbFill"></div>
</div>
<div class="pb-meta">
<span id="pbInfo">準備中</span>
<button class="pb-btn" id="pbReset" type="button">リスタート</button>
</div>
</div>
</div>
CSS
:root {
--bg: #0b1020;
--panel: #141a33;
--track: #232a4d;
--g1: #34d399;
--g2: #38bdf8;
--g3: #818cf8;
--txt: #eef1ff;
--muted: #8b94c4;
}
* { 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(800px 400px at 50% 120%, #15224a 0%, transparent 60%),
var(--bg);
color: var(--txt);
}
.pb-stage { padding: 24px; width: 100%; display: grid; place-items: center; }
.pb-panel {
width: min(440px, 86vw);
background: var(--panel);
border: 1px solid rgba(255, 255, 255, .07);
border-radius: 18px;
padding: 22px 24px;
box-shadow: 0 20px 50px rgba(0, 0, 0, .5);
}
.pb-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 14px;
}
.pb-label { font-size: 14px; letter-spacing: .02em; }
.pb-pct {
font-size: 22px;
font-weight: 700;
font-variant-numeric: tabular-nums;
background: linear-gradient(90deg, var(--g1), var(--g2), var(--g3));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.pb-track {
height: 12px;
border-radius: 999px;
background: var(--track);
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .5);
}
.pb-fill {
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--g1), var(--g2), var(--g3), var(--g1));
background-size: 220% 100%;
/* 進捗変化を滑らかに+グラデを流す */
transition: width .35s cubic-bezier(.4, 0, .2, 1);
animation: pb-flow 2.4s linear infinite;
}
@keyframes pb-flow { to { background-position: -220% 0; } }
.pb-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
font-size: 12px;
color: var(--muted);
}
.pb-panel.is-done .pb-label { color: var(--g1); }
.pb-panel.is-done .pb-fill { animation: none; }
.pb-btn {
border: 1px solid rgba(255, 255, 255, .14);
background: transparent;
color: var(--txt);
padding: 6px 14px;
border-radius: 999px;
font-size: 12px;
cursor: pointer;
transition: background .2s, transform .1s;
}
.pb-btn:hover { background: rgba(255, 255, 255, .08); }
.pb-btn:active { transform: scale(.95); }
@media (prefers-reduced-motion: reduce) {
.pb-fill { animation: none; transition: width .2s linear; }
}
JavaScript
// 不規則に進むプログレスバー(疑似アップロード)
const fill = document.getElementById('pbFill');
const pct = document.getElementById('pbPct');
const bar = document.getElementById('pbBar');
const info = document.getElementById('pbInfo');
const label = document.getElementById('pbLabel');
const panel = document.querySelector('.pb-panel');
const reset = document.getElementById('pbReset');
let value = 0;
let timer = null;
const total = 12.4; // 疑似ファイルサイズ(MB)
function tick() {
// ランダムな増分でリアルな進み方に
const step = Math.random() * 9 + 2;
value = Math.min(100, value + step);
render();
if (value >= 100) {
finish();
} else {
timer = setTimeout(tick, 380 + Math.random() * 320);
}
}
function render() {
const v = Math.round(value);
if (fill) fill.style.width = value + '%';
if (pct) pct.textContent = v + '%';
if (bar) bar.setAttribute('aria-valuenow', String(v));
if (info) info.textContent = (total * value / 100).toFixed(1) + ' / ' + total + ' MB';
}
function finish() {
panel?.classList.add('is-done');
if (label) label.textContent = '完了しました';
if (info) info.textContent = total + ' MB 受信済み';
}
function start() {
clearTimeout(timer);
value = 0;
panel?.classList.remove('is-done');
if (label) label.textContent = 'アップロード中…';
render();
timer = setTimeout(tick, 400);
}
reset?.addEventListener('click', start);
// 初回起動
start();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「プログレスバー」の効果を追加してください。
# 追加してほしい効果
プログレスバー(ローダー & スケルトン)
不規則に進む疑似アップロードをパーセントとMB表示で見せる進捗バー。グラデーションが流れ完了状態も色で表現します。fetch進捗の可視化に最適。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- プログレスバー: 進捗をパーセント表示・自動で進みグラデが流れる -->
<div class="pb-stage">
<div class="pb-panel">
<div class="pb-head">
<span class="pb-label" id="pbLabel">アップロード中…</span>
<span class="pb-pct" id="pbPct">0%</span>
</div>
<!-- 進捗トラック -->
<div class="pb-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="pbBar">
<div class="pb-fill" id="pbFill"></div>
</div>
<div class="pb-meta">
<span id="pbInfo">準備中</span>
<button class="pb-btn" id="pbReset" type="button">リスタート</button>
</div>
</div>
</div>
【CSS】
:root {
--bg: #0b1020;
--panel: #141a33;
--track: #232a4d;
--g1: #34d399;
--g2: #38bdf8;
--g3: #818cf8;
--txt: #eef1ff;
--muted: #8b94c4;
}
* { 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(800px 400px at 50% 120%, #15224a 0%, transparent 60%),
var(--bg);
color: var(--txt);
}
.pb-stage { padding: 24px; width: 100%; display: grid; place-items: center; }
.pb-panel {
width: min(440px, 86vw);
background: var(--panel);
border: 1px solid rgba(255, 255, 255, .07);
border-radius: 18px;
padding: 22px 24px;
box-shadow: 0 20px 50px rgba(0, 0, 0, .5);
}
.pb-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 14px;
}
.pb-label { font-size: 14px; letter-spacing: .02em; }
.pb-pct {
font-size: 22px;
font-weight: 700;
font-variant-numeric: tabular-nums;
background: linear-gradient(90deg, var(--g1), var(--g2), var(--g3));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.pb-track {
height: 12px;
border-radius: 999px;
background: var(--track);
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .5);
}
.pb-fill {
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--g1), var(--g2), var(--g3), var(--g1));
background-size: 220% 100%;
/* 進捗変化を滑らかに+グラデを流す */
transition: width .35s cubic-bezier(.4, 0, .2, 1);
animation: pb-flow 2.4s linear infinite;
}
@keyframes pb-flow { to { background-position: -220% 0; } }
.pb-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
font-size: 12px;
color: var(--muted);
}
.pb-panel.is-done .pb-label { color: var(--g1); }
.pb-panel.is-done .pb-fill { animation: none; }
.pb-btn {
border: 1px solid rgba(255, 255, 255, .14);
background: transparent;
color: var(--txt);
padding: 6px 14px;
border-radius: 999px;
font-size: 12px;
cursor: pointer;
transition: background .2s, transform .1s;
}
.pb-btn:hover { background: rgba(255, 255, 255, .08); }
.pb-btn:active { transform: scale(.95); }
@media (prefers-reduced-motion: reduce) {
.pb-fill { animation: none; transition: width .2s linear; }
}
【JavaScript】
// 不規則に進むプログレスバー(疑似アップロード)
const fill = document.getElementById('pbFill');
const pct = document.getElementById('pbPct');
const bar = document.getElementById('pbBar');
const info = document.getElementById('pbInfo');
const label = document.getElementById('pbLabel');
const panel = document.querySelector('.pb-panel');
const reset = document.getElementById('pbReset');
let value = 0;
let timer = null;
const total = 12.4; // 疑似ファイルサイズ(MB)
function tick() {
// ランダムな増分でリアルな進み方に
const step = Math.random() * 9 + 2;
value = Math.min(100, value + step);
render();
if (value >= 100) {
finish();
} else {
timer = setTimeout(tick, 380 + Math.random() * 320);
}
}
function render() {
const v = Math.round(value);
if (fill) fill.style.width = value + '%';
if (pct) pct.textContent = v + '%';
if (bar) bar.setAttribute('aria-valuenow', String(v));
if (info) info.textContent = (total * value / 100).toFixed(1) + ' / ' + total + ' MB';
}
function finish() {
panel?.classList.add('is-done');
if (label) label.textContent = '完了しました';
if (info) info.textContent = total + ' MB 受信済み';
}
function start() {
clearTimeout(timer);
value = 0;
panel?.classList.remove('is-done');
if (label) label.textContent = 'アップロード中…';
render();
timer = setTimeout(tick, 400);
}
reset?.addEventListener('click', start);
// 初回起動
start();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。