プログレスバー 2 — ファイルアップロード

表示・インジケーター 中級

このコンポーネントについて

ファイルアップロードのプログレスバーは、ユーザーがファイルを選択してサーバーへ送信する際に、転送の進捗を視覚的に伝えるUIです。大きなファイルのアップロードは時間がかかるため、進捗が見えないと「送れているのか?止まっているのか?」とユーザーが不安になります。

実際のサーバー転送には XMLHttpRequest(XHR)の upload.onprogress イベントを使います。このページのデモはサーバーなしで動作確認できるよう FileReader のローカル読み込み進捗を使用しており、サンプルソースに実サーバー向けのXHRコードをコメントアウトで併記しています。

  • ファイル選択 — ファイルを選択するとファイル名・サイズを表示し、「アップロード開始」ボタンが活性化する
  • 進捗リアルタイム表示 — 読み込み中の進捗率(0〜100%)をバーとパーセンテージで表示する
  • デモはローカル読み込み — ファイルは外部に転送されず、ブラウザ内の読み込み進捗を表示する(サーバー不要)
  • 実装コメント付き — サンプルJSに実サーバー向けのXHRコードをコメントアウトで併記

実装のポイント・注意点

実際のサーバーへのアップロード進捗を取得するには XMLHttpRequestxhr.upload.onprogress を使います。fetch API には標準的な進捗取得の仕組みがないため、アップロード進捗が必要な場合は XHR を選んでください。onprogress イベントの e.lengthComputabletrue のときだけ e.loaded / e.total で進捗率を計算できます。サーバーが Content-Length ヘッダーを返さない場合は false になります。

このページのデモは FileReader.readAsArrayBuffer でファイルをメモリに読み込み、その読み込み進捗を表示しています。FileReader.onprogress は小さなファイル(数百KB以下)では発火しないことがあるため、1MB以上のファイルでお試しください。また、リセット時に input.value = '' でファイル選択をクリアしないと、同じファイルを再選択しても change イベントが発火しない点に注意してください。

デモ

※ このデモは擬似的なアップロードです。ファイルは外部に転送されません。
進捗はブラウザ内でのローカル読み込み進捗を表示しています。
(目安:1MB以上のファイルで進捗が見えやすくなります)

サンプルソース

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>

<!-- ========================================
  プログレスバー — ファイルアップロード
  FileReader でローカル読み込み進捗を取得する
  (このデモでは外部サーバーへの転送は行いません)
======================================== -->
<div class="fu-wrap">

  <!-- デモ注釈 -->
  <p class="fu-demo-note">
    ※ このデモは擬似的なアップロードです。ファイルは外部に転送されません。<br>
    進捗はブラウザ内でのローカル読み込み進捗を表示しています。<br>
    (目安:1MB以上のファイルで進捗が見えやすくなります)
  </p>

  <!-- ファイル選択 -->
  <label class="fu-file-label" for="fu-file-input">
    クリックしてファイルを選択(1MB以上推奨)
    <input type="file" id="fu-file-input">
  </label>
  <p class="fu-file-info" id="fu-file-info"></p>

  <!-- プログレスバー -->
  <div class="fu-bar-wrap" id="fu-bar-wrap" hidden>
    <div class="fu-bar" id="fu-bar"></div>
  </div>
  <p class="fu-percent" id="fu-percent"></p>

  <!-- ステータス -->
  <p class="fu-status" id="fu-status"></p>

  <!-- 操作ボタン -->
  <div class="fu-controls">
    <button id="fu-start-btn" type="button" onclick="startUpload()" disabled>アップロード開始</button>
    <button id="fu-reset-btn" type="button" onclick="resetUpload()" hidden>リセット</button>
  </div>

</div>

<script src="./script.js"></script>
</body>
</html>
/* ファイルアップロード プログレスバー — style.css */
:root {
  --bar-color:   #2563EB;  /* バー通常色(青) */
  --bar-done:    #16A34A;  /* バー完了色(緑) */
  --bar-bg:      #E5E7EB;  /* バー外枠の背景 */
  --note-bg:     #FEF3C7;  /* 注釈ボックス背景 */
  --note-border: #F59E0B;  /* 注釈ボックスのアクセント */
  --note-text:   #92400E;  /* 注釈テキスト色 */
}

*, *::before, *::after { box-sizing: border-box; }

body {
  font-family: sans-serif;
  padding: 24px;
  max-width: 520px;
  margin: 0 auto;
  background: #fff;
  color: #1A2332;
}

/* デモ注釈 */
.fu-demo-note {
  padding: 10px 14px;
  background: var(--note-bg);
  border-left: 4px solid var(--note-border);
  border-radius: 0 6px 6px 0;
  font-size: 13px;
  color: var(--note-text);
  line-height: 1.7;
  margin-bottom: 20px;
}

/* ファイル選択ラベル */
.fu-file-label {
  display: block;
  padding: 20px;
  border: 2px dashed #CBD5E1;
  border-radius: 8px;
  text-align: center;
  cursor: pointer;
  font-size: 14px;
  color: #5A6A7A;
  transition: border-color 0.15s, background 0.15s;
  margin-bottom: 10px;
}
.fu-file-label:hover {
  border-color: #2563EB;
  background: #EFF6FF;
}
/* inputはラベルクリックで起動するため非表示 */
.fu-file-label input[type="file"] { display: none; }

/* 選択ファイル情報 */
.fu-file-info {
  font-size: 13px;
  color: #374151;
  margin: 0 0 16px;
  min-height: 1.4em;
}

/* バー外枠 */
.fu-bar-wrap {
  background: var(--bar-bg);
  border-radius: 9999px;
  height: 14px;
  overflow: hidden;
  margin-bottom: 6px;
}

/* バー本体 */
.fu-bar {
  height: 100%;
  width: 0%;
  background: var(--bar-color);
  border-radius: 9999px;
  transition: width 0.2s ease;
}
.fu-bar.done { background: var(--bar-done); }

/* パーセンテージ */
.fu-percent {
  font-size: 13px;
  font-weight: 700;
  text-align: right;
  color: #374151;
  margin: 0 0 10px;
  min-height: 1.4em;
}

/* ステータス */
.fu-status {
  font-size: 13px;
  color: #5A6A7A;
  margin: 0 0 16px;
  min-height: 1.4em;
}

/* ボタン群 */
.fu-controls { display: flex; gap: 8px; flex-wrap: wrap; }

.fu-controls button {
  padding: 8px 18px;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  font-family: sans-serif;
  transition: background 0.15s, opacity 0.15s;
}

#fu-start-btn {
  background: #2563EB;
  color: #fff;
  border: none;
}
#fu-start-btn:hover:not(:disabled) { background: #1D4ED8; }

#fu-reset-btn {
  background: #fff;
  color: #5A6A7A;
  border: 1.5px solid #D0D7E0;
}
#fu-reset-btn:hover { background: #F4F6F9; }

button:disabled { opacity: 0.4; cursor: not-allowed; }

@media (max-width: 480px) {
  body { padding: 16px; }
  .fu-file-label { padding: 16px; }
}
// ファイルアップロード プログレスバー
var fileInput    = document.getElementById('fu-file-input');
var selectedFile = null;

// ファイル選択時の処理
fileInput.addEventListener('change', function () {
  selectedFile = fileInput.files[0];
  if (!selectedFile) return;
  var sizeMB = (selectedFile.size / 1024 / 1024).toFixed(2);
  document.getElementById('fu-file-info').textContent =
    selectedFile.name + '(' + sizeMB + ' MB)';
  document.getElementById('fu-start-btn').disabled = false;
  resetBarUI(); // 前回の進捗表示をクリアする
});

// アップロード開始
function startUpload() {
  if (!selectedFile) return;
  document.getElementById('fu-start-btn').disabled = true;
  document.getElementById('fu-bar-wrap').hidden = false;
  document.getElementById('fu-status').textContent = '読み込み中...';

  // ── デモ用:FileReader でローカル読み込み進捗を取得 ──
  var reader = new FileReader();

  reader.onprogress = function (e) {
    if (e.lengthComputable) {
      var percent = Math.round((e.loaded / e.total) * 100);
      updateBar(percent);
    }
  };

  reader.onloadend = function () {
    updateBar(100);
    document.getElementById('fu-bar').classList.add('done');
    document.getElementById('fu-status').textContent = 'アップロード完了!';
    document.getElementById('fu-reset-btn').hidden = false;
  };

  reader.readAsArrayBuffer(selectedFile);

  // ── 実際のサーバーへアップロードする場合はこちら(XHR使用) ──
  // ※ fetch はアップロード進捗を取得できないため XHR を使用する
  // const xhr = new XMLHttpRequest();
  // xhr.upload.onprogress = (e) => {
  //   if (e.lengthComputable) {
  //     const percent = Math.round((e.loaded / e.total) * 100);
  //     updateBar(percent);
  //   }
  // };
  // xhr.onloadend = () => {
  //   if (xhr.status === 200) {
  //     updateBar(100);
  //     document.getElementById('fu-bar').classList.add('done');
  //     document.getElementById('fu-status').textContent = 'アップロード完了!';
  //     document.getElementById('fu-reset-btn').hidden = false;
  //   }
  // };
  // const formData = new FormData();
  // formData.append('file', selectedFile);
  // xhr.open('POST', 'https://your-server.com/upload');
  // xhr.send(formData);
}

// バーとパーセンテージを更新する
function updateBar(percent) {
  document.getElementById('fu-bar').style.width = percent + '%';
  document.getElementById('fu-percent').textContent = percent + '%';
}

// バー表示だけを初期状態に戻す(ファイル選択は保持)
function resetBarUI() {
  var bar = document.getElementById('fu-bar');
  bar.style.width = '0%';
  bar.classList.remove('done');
  document.getElementById('fu-percent').textContent = '';
  document.getElementById('fu-status').textContent  = '';
  document.getElementById('fu-bar-wrap').hidden     = true;
  document.getElementById('fu-reset-btn').hidden    = true;
}

// 全体リセット(ファイル選択もクリア)
function resetUpload() {
  selectedFile = null;
  // value をクリアしないと同じファイルを再選択しても change が発火しない
  fileInput.value = '';
  document.getElementById('fu-file-info').textContent = '';
  document.getElementById('fu-start-btn').disabled    = true;
  resetBarUI();
}

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定やサーバーURL変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# プログレスバー(ファイルアップロード)作成依頼

## 概要
ファイルを選択してアップロードする際の進捗をリアルタイム表示するプログレスバーを実装してください。
デモはサーバーなしで動作確認できるよう FileReader のローカル読み込み進捗を使用し、
実際のサーバー転送向けに XMLHttpRequest のコードをコメントアウトで併記してください。

## 要件
- ファイル選択ボタンでローカルファイルを選択できる
- 選択後にファイル名とファイルサイズを表示する
- 「アップロード開始」ボタンで処理を開始する(ファイル未選択時は非活性)
- プログレスバーに進捗率(0〜100%)をリアルタイム表示する
- 「このデモはローカル読み込み進捗です。ファイルは外部に転送されません。」の注釈を目立つ色で表示する
- 完了後に完了メッセージを表示してリセットできる

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
デモ実装(サーバー不要):
FileReader.readAsArrayBuffer でファイルをメモリに読み込み、onprogress イベントで
e.loaded / e.total * 100 を計算してバーを更新する。onloadend で 100% にして完了表示。

実サーバー向け実装(コメントアウトで併記):
XMLHttpRequest を使い xhr.upload.onprogress で進捗を取得する。
e.lengthComputable が true のとき Math.round(e.loaded / e.total * 100) で進捗率を算出する。
FormData にファイルを追加し xhr.open('POST', 'URL') → xhr.send(formData) で送信する。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。