プログレスバー

不規則に進む疑似アップロードをパーセントとMB表示で見せる進捗バー。グラデーションが流れ完了状態も色で表現します。fetch進捗の可視化に最適。

#css#javascript#progress

ライブデモ

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