JavaScript Drag & Drop API 完全攻略:ファイルアップロード UI を最速で作る
Web アプリケーションでファイルをアップロードする際、従来の「ファイル選択」ボタンだけでは物足りないと感じたことはありませんか。現代の Web では、ドラッグ&ドロップでファイルをアップロードできる UI が当たり前になってきました。
この記事では、JavaScript の Drag & Drop API を使って、実用的なファイルアップロード UI を最速で実装する方法をご紹介します。基本的な仕組みから、エラーハンドリング、プレビュー機能まで、実務で使える知識を段階的に解説していきますね。
背景
Web におけるファイルアップロードの進化
Web におけるファイルアップロードは、以下のような進化を遂げてきました。
| # | 時代 | アップロード方法 | ユーザビリティ |
|---|---|---|---|
| 1 | 2000 年代前半 | <input type="file"> のみ | ★☆☆☆☆ |
| 2 | 2000 年代後半 | Flash/Java アプレット | ★★☆☆☆ |
| 3 | 2010 年代前半 | HTML5 File API | ★★★☆☆ |
| 4 | 2010 年代後半〜現在 | Drag & Drop API + File API | ★★★★★ |
初期の Web ではファイル選択ボタンをクリックして、OS のファイル選択ダイアログから選ぶしか方法がありませんでした。しかし HTML5 の登場により、ブラウザ上でのドラッグ&ドロップが標準機能として実装され、ユーザー体験が飛躍的に向上したのです。
Drag & Drop API の基本概念
Drag & Drop API は、以下の要素で構成されています。
下図は、ドラッグ&ドロップの基本フローを示したものです。
mermaidflowchart TB
user["ユーザー"] -->|ファイルをドラッグ| dragenter["dragenter<br/>イベント発火"]
dragenter -->|ドロップ領域に入る| dragover["dragover<br/>イベント発火"]
dragover -->|領域内を移動中| dragover
dragover -->|領域から出る| dragleave["dragleave<br/>イベント発火"]
dragover -->|ファイルを放す| drop["drop<br/>イベント発火"]
drop -->|ファイル情報取得| fileapi["File API<br/>でファイル処理"]
図で理解できる要点:
- ドラッグ&ドロップは 4 つの主要イベントで構成される
- 各イベントが連鎖的に発火し、最終的に drop でファイルを取得
- File API と組み合わせることで実用的な処理が可能になる
各イベントの役割は以下の通りです。
| # | イベント | 発火タイミング | 主な用途 |
|---|---|---|---|
| 1 | dragenter | ドロップ領域に入った時 | 視覚的フィードバックの開始 |
| 2 | dragover | ドロップ領域内を移動中 | デフォルト動作の無効化 |
| 3 | dragleave | ドロップ領域から出た時 | 視覚的フィードバックの解除 |
| 4 | drop | ファイルを放した時 | ファイル情報の取得・処理 |
この 4 つのイベントを正しく扱うことが、Drag & Drop API を使いこなす第一歩となります。
課題
従来のファイルアップロードにおける問題点
従来の <input type="file"> だけを使ったアップロード UI には、いくつかの課題がありました。
以下の図は、従来方式とドラッグ&ドロップ方式の操作フローを比較したものです。
mermaidflowchart LR
subgraph old ["従来方式(クリック選択)"]
direction TB
step1["ボタンをクリック"] --> step2["ダイアログが開く"]
step2 --> step3["フォルダを探す"]
step3 --> step4["ファイルを選択"]
step4 --> step5["OK をクリック"]
end
subgraph new ["ドラッグ&ドロップ方式"]
direction TB
dstep1["ファイルをドラッグ"] --> dstep2["ドロップ領域に放す"]
dstep2 --> dstep3["即座にアップロード"]
end
old -.->|操作ステップ: 5段階| result1["時間がかかる"]
new -.->|操作ステップ: 2段階| result2["直感的で高速"]
図で理解できる要点:
- 従来方式は 5 段階の操作が必要で煩雑
- ドラッグ&ドロップは 2 段階で完了し、ユーザビリティが大幅に向上
- 操作の簡素化により、ユーザーの離脱率も低下
実装時の技術的課題
Drag & Drop API を実装する際には、以下のような技術的課題があります。
| # | 課題 | 影響 | 対策の必要性 |
|---|---|---|---|
| 1 | デフォルト動作の防止 | ブラウザがファイルを開いてしまう | ★★★★★ |
| 2 | 視覚的フィードバック | ユーザーが操作を理解できない | ★★★★☆ |
| 3 | ファイル検証 | 不正なファイルがアップロードされる | ★★★★★ |
| 4 | エラーハンドリング | ユーザーに適切なエラーが伝わらない | ★★★★☆ |
| 5 | クロスブラウザ対応 | 古いブラウザで動作しない | ★★★☆☆ |
特に、デフォルト動作の防止を忘れると、ファイルをドロップした際にブラウザがそのファイルを開いてしまうため、必ず preventDefault() を呼び出す必要があります。
解決策
基本的な実装アプローチ
Drag & Drop API を使ったファイルアップロード UI を実装するには、以下のステップで進めていきます。
下図は、実装の全体フローを示したものです。
mermaidflowchart TD
start["実装開始"] --> html["HTML構造の構築"]
html --> css["CSS でスタイリング"]
css --> events["イベントリスナーの設定"]
events --> prevent["デフォルト動作の防止"]
prevent --> visual["視覚的フィードバック"]
visual --> filepro["ファイル処理ロジック"]
filepro --> valid["ファイル検証"]
valid --> error["エラーハンドリング"]
error --> preview["プレビュー機能(任意)"]
preview --> done["実装完了"]
図で理解できる要点:
- 実装は HTML/CSS → イベント処理 → ファイル処理という流れで進める
- 各ステップが依存関係にあり、順序を守ることが重要
- 視覚的フィードバックとエラーハンドリングがユーザー体験を左右する
それでは、具体的な実装方法を見ていきましょう。
HTML 構造の構築
まず、ドロップ領域となる HTML 要素を作成します。
html<!-- ドロップ領域のコンテナ -->
<div id="dropArea" class="drop-area">
<p>ここにファイルをドラッグ&ドロップ</p>
<p>または</p>
<input type="file" id="fileInput" multiple hidden />
<button id="selectButton">ファイルを選択</button>
</div>
このコードでは、ドロップ領域と従来のファイル選択ボタンの両方を用意しています。hidden 属性により input 要素は非表示にし、カスタムボタンから呼び出す形にすることで、デザインの自由度を高めることができますね。
次に、アップロードされたファイルのプレビューを表示する領域を追加します。
html<!-- ファイルプレビュー領域 -->
<div id="fileList" class="file-list">
<!-- ここに選択されたファイルの情報が表示される -->
</div>
CSS によるスタイリング
ドロップ領域に視覚的なフィードバックを提供するためのスタイルを定義します。
css/* ドロップ領域の基本スタイル */
.drop-area {
width: 100%;
max-width: 600px;
padding: 40px;
border: 3px dashed #ccc;
border-radius: 8px;
text-align: center;
background-color: #f9f9f9;
transition: all 0.3s ease;
cursor: pointer;
}
このスタイルにより、ドロップ領域が明確に視覚化されます。点線の枠線と背景色により、ユーザーはここがドロップ可能な領域であることを直感的に理解できるでしょう。
ドラッグ中の状態を示すスタイルを追加します。
css/* ドラッグ中のハイライト表示 */
.drop-area.drag-over {
border-color: #4caf50;
background-color: #e8f5e9;
transform: scale(1.02);
}
/* ファイル選択ボタンのスタイル */
#selectButton {
padding: 12px 24px;
font-size: 16px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 16px;
}
#selectButton:hover {
background-color: #1976d2;
}
.drag-over クラスを使うことで、ファイルがドロップ領域の上にある時に色が変わり、ユーザーに「今ここで放せばアップロードできる」というフィードバックを提供できます。
ファイルリストの表示スタイルも定義しましょう。
css/* ファイルリストのスタイル */
.file-list {
margin-top: 24px;
max-width: 600px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
}
.file-item img {
width: 50px;
height: 50px;
object-fit: cover;
margin-right: 12px;
border-radius: 4px;
}
イベントリスナーの設定
JavaScript で各イベントリスナーを設定していきます。まず、基本的な DOM 要素の取得から始めましょう。
javascript// DOM 要素の取得
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const selectButton =
document.getElementById('selectButton');
const fileList = document.getElementById('fileList');
次に、ファイル選択ボタンのクリックイベントを設定します。
javascript// ファイル選択ボタンのイベント
// クリックすると非表示の input 要素をトリガーする
selectButton.addEventListener('click', () => {
fileInput.click();
});
// input 要素でファイルが選択された時の処理
fileInput.addEventListener('change', (e) => {
const files = e.target.files;
handleFiles(files);
});
このコードにより、カスタムボタンをクリックすると、隠された <input type="file"> が開かれます。これで、ドラッグ&ドロップと従来のファイル選択の両方に対応できるようになりますね。
デフォルト動作の防止
Drag & Drop API で最も重要なのが、ブラウザのデフォルト動作を防止することです。
javascript// dragenter イベント: ドロップ領域に入った時
// デフォルト動作を防止し、視覚的フィードバックを追加
dropArea.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
dropArea.classList.add('drag-over');
});
preventDefault() を呼び出すことで、ブラウザがファイルを開いてしまうのを防ぎます。また、stopPropagation() でイベントの伝播を止めることで、親要素への影響を防げるのです。
dragover イベントも同様に処理します。
javascript// dragover イベント: ドロップ領域内を移動中
// このイベントは連続的に発火するため、必ず preventDefault() が必要
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
dragover は、ドラッグ中に連続的に発火します。ここで preventDefault() を忘れると、drop イベントが発火しなくなってしまうため、必ず設定しましょう。
ドロップ領域から出た時の処理も追加します。
javascript// dragleave イベント: ドロップ領域から出た時
// 視覚的フィードバックを解除
dropArea.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
dropArea.classList.remove('drag-over');
});
ファイルのドロップ処理
最も重要な drop イベントの処理を実装します。
javascript// drop イベント: ファイルがドロップされた時
// ここでファイル情報を取得し、処理を開始
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
// 視覚的フィードバックを解除
dropArea.classList.remove('drag-over');
// ドロップされたファイルを取得
const files = e.dataTransfer.files;
// ファイル処理関数を呼び出す
handleFiles(files);
});
e.dataTransfer.files からドロップされたファイルの情報を取得できます。この files は FileList オブジェクトで、配列のように扱うことができますね。
ファイル処理ロジックの実装
取得したファイルを処理する関数を実装しましょう。
javascript/**
* ファイル処理のメイン関数
* @param {FileList} files - 処理するファイルのリスト
*/
function handleFiles(files) {
// FileList を配列に変換して処理
const fileArray = Array.from(files);
// 各ファイルに対して処理を実行
fileArray.forEach((file) => {
// ファイルの検証
if (validateFile(file)) {
// ファイルをプレビュー表示
displayFile(file);
// 実際のアップロード処理(任意)
uploadFile(file);
}
});
}
Array.from() を使って FileList を配列に変換することで、forEach などの配列メソッドが使えるようになります。これにより、複数ファイルの処理が簡潔に記述できるのです。
ファイル検証の実装
ファイルのタイプやサイズを検証する関数を作成します。
javascript/**
* ファイルの検証を行う
* @param {File} file - 検証するファイル
* @returns {boolean} 検証結果(true: 有効, false: 無効)
*/
function validateFile(file) {
// 許可するファイルタイプ(MIME タイプ)
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
];
// 最大ファイルサイズ(5MB)
const maxSize = 5 * 1024 * 1024;
// ファイルタイプの検証
if (!allowedTypes.includes(file.type)) {
showError(
`${file.name} は許可されていないファイル形式です。`
);
return false;
}
// ファイルサイズの検証
if (file.size > maxSize) {
showError(`${file.name} は 5MB を超えています。`);
return false;
}
return true;
}
MIME タイプによる検証は、セキュリティ上重要です。ただし、クライアント側の検証だけでは不十分なため、サーバー側でも必ず検証を行いましょう。
エラーメッセージを表示する関数も実装します。
javascript/**
* エラーメッセージを表示する
* @param {string} message - 表示するエラーメッセージ
*/
function showError(message) {
// エラーメッセージ要素を作成
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
errorDiv.style.cssText = `
padding: 12px;
margin: 8px 0;
background-color: #ffebee;
color: #c62828;
border-radius: 4px;
border-left: 4px solid #c62828;
`;
// ドロップ領域の後に挿入
dropArea.parentNode.insertBefore(
errorDiv,
dropArea.nextSibling
);
// 3秒後に自動的に削除
setTimeout(() => {
errorDiv.remove();
}, 3000);
}
ファイルプレビューの実装
画像ファイルの場合、サムネイルをプレビュー表示すると便利です。
javascript/**
* ファイルをプレビュー表示する
* @param {File} file - 表示するファイル
*/
function displayFile(file) {
// ファイルアイテムのコンテナを作成
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
// 画像ファイルの場合、サムネイルを表示
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
// FileReader を使って画像データを読み込む
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
fileItem.appendChild(img);
}
// ファイル情報を表示する要素を作成
const fileInfo = createFileInfo(file);
fileItem.appendChild(fileInfo);
// ファイルリストに追加
fileList.appendChild(fileItem);
}
FileReader API を使うことで、ファイルをブラウザ上で読み込み、プレビュー表示ができます。readAsDataURL() メソッドは、ファイルを Base64 エンコードされた Data URL に変換してくれるのです。
ファイル情報を表示する要素を作成する関数も実装しましょう。
javascript/**
* ファイル情報を表示する要素を作成
* @param {File} file - ファイルオブジェクト
* @returns {HTMLElement} ファイル情報要素
*/
function createFileInfo(file) {
const infoDiv = document.createElement('div');
infoDiv.className = 'file-info';
// ファイル名
const fileName = document.createElement('div');
fileName.className = 'file-name';
fileName.textContent = file.name;
fileName.style.fontWeight = 'bold';
// ファイルサイズを人間が読みやすい形式に変換
const fileSize = document.createElement('div');
fileSize.className = 'file-size';
fileSize.textContent = formatFileSize(file.size);
fileSize.style.color = '#666';
fileSize.style.fontSize = '14px';
infoDiv.appendChild(fileName);
infoDiv.appendChild(fileSize);
return infoDiv;
}
ファイルサイズをわかりやすく表示するヘルパー関数を作成します。
javascript/**
* ファイルサイズを読みやすい形式に変換
* @param {number} bytes - バイト数
* @returns {string} フォーマットされたサイズ(例: "1.5 MB")
*/
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (
Math.round((bytes / Math.pow(k, i)) * 100) / 100 +
' ' +
sizes[i]
);
}
このヘルパー関数により、「5242880 bytes」のような数値が「5 MB」のように読みやすく表示されます。ユーザー体験の向上につながる重要な工夫ですね。
ファイルアップロード処理
最後に、実際にサーバーへファイルをアップロードする処理を実装します。
javascript/**
* ファイルをサーバーにアップロード
* @param {File} file - アップロードするファイル
*/
async function uploadFile(file) {
// FormData オブジェクトを作成
const formData = new FormData();
formData.append('file', file);
try {
// fetch API でファイルを送信
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
// レスポンスの処理
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`
);
}
const result = await response.json();
console.log('アップロード成功:', result);
} catch (error) {
console.error('アップロードエラー:', error);
showError(
`${file.name} のアップロードに失敗しました。`
);
}
}
FormData を使うことで、ファイルを簡単にサーバーへ送信できます。fetch API と組み合わせることで、モダンで読みやすいコードになりますね。
アップロード進捗を表示する拡張版も見てみましょう。
javascript/**
* 進捗表示付きファイルアップロード
* @param {File} file - アップロードするファイル
* @param {HTMLElement} progressElement - 進捗バー要素
*/
function uploadFileWithProgress(file, progressElement) {
const formData = new FormData();
formData.append('file', file);
// XMLHttpRequest を使用(fetch は進捗イベントに対応していないため)
const xhr = new XMLHttpRequest();
// 進捗イベントのリスナー
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
// 進捗率を計算(0-100)
const percentComplete = (e.loaded / e.total) * 100;
// 進捗バーを更新
progressElement.style.width = percentComplete + '%';
progressElement.textContent =
Math.round(percentComplete) + '%';
}
});
// 完了時の処理
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
console.log('アップロード完了:', xhr.responseText);
progressElement.style.backgroundColor = '#4CAF50';
} else {
showError(`アップロードエラー: ${xhr.status}`);
progressElement.style.backgroundColor = '#f44336';
}
});
// エラー時の処理
xhr.addEventListener('error', () => {
showError('ネットワークエラーが発生しました。');
progressElement.style.backgroundColor = '#f44336';
});
// リクエストを送信
xhr.open('POST', '/api/upload');
xhr.send(formData);
}
進捗表示を実装する場合は、fetch ではなく XMLHttpRequest を使う必要があります。これにより、大きなファイルのアップロード時にユーザーが進捗を確認できるようになるのです。
具体例
実践的なファイルアップロード UI の実装
ここまで学んだ内容を統合して、実務で使える完全なファイルアップロード UI を構築していきます。
下図は、完成形の UI とデータフローを示したものです。
mermaidflowchart TD
user["ユーザー"] -->|ドラッグ&ドロップ| drop["Drop イベント"]
user -->|クリック選択| click["Click イベント"]
drop --> validate["ファイル検証"]
click --> validate
validate -->|OK| preview["プレビュー表示"]
validate -->|NG| error["エラー表示"]
preview --> upload["サーバーへ<br/>アップロード"]
upload --> progress["進捗表示"]
progress --> success["完了通知"]
error --> retry["再試行を促す"]
図で理解できる要点:
- ドラッグ&ドロップとクリック選択の両方に対応
- ファイル検証を必ず通過させることでセキュリティを確保
- プレビュー → アップロード → 進捗表示という流れでユーザー体験を向上
完全な HTML 構造
実用的な UI の HTML 構造を作成します。
html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>ファイルアップロード - Drag & Drop</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<h1>ファイルアップロード</h1>
<!-- ドロップ領域 -->
<div id="dropArea" class="drop-area">
<svg
class="upload-icon"
width="64"
height="64"
viewBox="0 0 24 24"
>
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
</svg>
<p class="drop-text">
ファイルをここにドラッグ&ドロップ
</p>
<p class="drop-subtext">または</p>
<input
type="file"
id="fileInput"
multiple
accept="image/*"
hidden
/>
<button id="selectButton" class="select-button">
ファイルを選択
</button>
<p class="file-hint">
画像ファイル(JPEG, PNG, GIF, WebP)/ 最大 5MB
</p>
</div>
<!-- ファイルプレビューエリア -->
<div id="fileList" class="file-list"></div>
</div>
<script src="app.js"></script>
</body>
</html>
この HTML では、アイコンや説明文を含めた、よりわかりやすい UI を構築しています。accept 属性により、ファイル選択ダイアログで画像ファイルのみがフィルタリングされるのです。
完全な CSS スタイル
プロフェッショナルな見た目を実現する CSS を実装します。
css/* 基本スタイル */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont,
'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: #333;
margin-bottom: 24px;
text-align: center;
}
次に、ドロップ領域のスタイルを定義します。
css/* ドロップ領域 */
.drop-area {
background-color: #fff;
border: 3px dashed #d0d0d0;
border-radius: 12px;
padding: 48px 24px;
text-align: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.drop-area:hover {
border-color: #2196f3;
background-color: #f8f9fa;
}
.drop-area.drag-over {
border-color: #4caf50;
background-color: #e8f5e9;
transform: scale(1.02);
box-shadow: 0 8px 16px rgba(76, 175, 80, 0.2);
}
.upload-icon {
fill: #999;
margin-bottom: 16px;
transition: fill 0.3s ease;
}
.drop-area:hover .upload-icon,
.drop-area.drag-over .upload-icon {
fill: #4caf50;
}
ホバーやドラッグ時のアニメーションを追加することで、ユーザーにリアルタイムなフィードバックを提供できます。cubic-bezier を使った滑らかなトランジションが心地よい操作感を生み出しますね。
テキストとボタンのスタイルも設定しましょう。
css/* テキストスタイル */
.drop-text {
font-size: 18px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.drop-subtext {
color: #999;
margin: 16px 0;
font-size: 14px;
}
.file-hint {
color: #666;
font-size: 12px;
margin-top: 16px;
}
/* ボタンスタイル */
.select-button {
background-color: #2196f3;
color: white;
border: none;
padding: 12px 32px;
font-size: 16px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s
ease;
font-weight: 500;
}
.select-button:hover {
background-color: #1976d2;
transform: translateY(-1px);
}
.select-button:active {
transform: translateY(0);
}
ファイルリストの表示スタイルを実装します。
css/* ファイルリスト */
.file-list {
margin-top: 32px;
}
.file-item {
display: flex;
align-items: center;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
transition: box-shadow 0.3s ease;
}
.file-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.file-item img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
margin-right: 16px;
border: 1px solid #e0e0e0;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.file-size {
font-size: 14px;
color: #666;
}
進捗バーのスタイルも追加しましょう。
css/* 進捗バー */
.progress-container {
width: 100%;
height: 6px;
background-color: #e0e0e0;
border-radius: 3px;
margin-top: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: #2196f3;
border-radius: 3px;
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: white;
font-weight: 500;
}
/* エラーメッセージ */
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 12px 16px;
border-radius: 6px;
border-left: 4px solid #c62828;
margin: 16px 0;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
完全な JavaScript 実装
すべての機能を統合した JavaScript コードを実装します。まず、初期化処理から始めましょう。
javascript// DOM 要素の取得と初期化
document.addEventListener('DOMContentLoaded', () => {
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const selectButton =
document.getElementById('selectButton');
const fileList = document.getElementById('fileList');
// イベントリスナーの設定
initializeEventListeners();
});
イベントリスナーを一括で設定する関数を作成します。
javascript/**
* すべてのイベントリスナーを初期化
*/
function initializeEventListeners() {
// ファイル選択ボタン
selectButton.addEventListener('click', () => {
fileInput.click();
});
// input 要素でのファイル選択
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
// ドラッグ&ドロップイベント
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(
(eventName) => {
dropArea.addEventListener(
eventName,
preventDefaults,
false
);
}
);
// ハイライト表示の制御
['dragenter', 'dragover'].forEach((eventName) => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach((eventName) => {
dropArea.addEventListener(
eventName,
unhighlight,
false
);
});
// ファイルドロップ時の処理
dropArea.addEventListener('drop', handleDrop, false);
}
配列の forEach を使うことで、複数のイベントに同じ処理を適用できます。コードの重複を避け、メンテナンス性が向上するのです。
デフォルト動作を防止する関数を実装します。
javascript/**
* ブラウザのデフォルト動作を防止
* @param {Event} e - イベントオブジェクト
*/
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
/**
* ドロップ領域をハイライト
*/
function highlight() {
dropArea.classList.add('drag-over');
}
/**
* ドロップ領域のハイライトを解除
*/
function unhighlight() {
dropArea.classList.remove('drag-over');
}
ドロップイベントのハンドラを実装します。
javascript/**
* ファイルドロップ時の処理
* @param {DragEvent} e - ドラッグイベント
*/
function handleDrop(e) {
// DataTransfer からファイルを取得
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
ファイル処理のメインロジックを実装します。
javascript/**
* ファイル処理のメイン関数
* @param {FileList} files - 処理するファイルリスト
*/
function handleFiles(files) {
// FileList を配列に変換
const fileArray = [...files];
// 各ファイルを処理
fileArray.forEach((file) => {
// ファイル検証
if (validateFile(file)) {
// UI に表示
displayFilePreview(file);
// サーバーにアップロード
uploadFileWithProgress(file);
}
});
}
/**
* ファイルの検証
* @param {File} file - 検証するファイル
* @returns {boolean} 検証結果
*/
function validateFile(file) {
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
];
const maxSize = 5 * 1024 * 1024; // 5MB
// ファイルタイプの確認
if (!allowedTypes.includes(file.type)) {
showError(
`"${file.name}" は対応していない形式です。` +
`対応形式: JPEG, PNG, GIF, WebP`
);
return false;
}
// ファイルサイズの確認
if (file.size > maxSize) {
showError(
`"${file.name}" のサイズが大きすぎます。` +
`最大サイズ: 5MB (現在: ${formatFileSize(
file.size
)})`
);
return false;
}
return true;
}
スプレッド構文(...)を使うことで、FileList を配列に変換できます。これにより、配列の各種メソッドが使えるようになり、コードがより簡潔になりますね。
ファイルプレビューを表示する関数を実装します。
javascript/**
* ファイルのプレビューを表示
* @param {File} file - 表示するファイル
*/
function displayFilePreview(file) {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.id = `file-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`;
// 画像プレビュー
const img = document.createElement('img');
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
fileItem.appendChild(img);
// ファイル情報
const fileInfo = document.createElement('div');
fileInfo.className = 'file-info';
const fileName = document.createElement('div');
fileName.className = 'file-name';
fileName.textContent = file.name;
const fileSize = document.createElement('div');
fileSize.className = 'file-size';
fileSize.textContent = formatFileSize(file.size);
fileInfo.appendChild(fileName);
fileInfo.appendChild(fileSize);
// 進捗バーコンテナ
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
progressBar.style.width = '0%';
progressContainer.appendChild(progressBar);
fileInfo.appendChild(progressContainer);
fileItem.appendChild(fileInfo);
fileList.appendChild(fileItem);
// 進捗バー要素を返す(アップロード処理で使用)
return progressBar;
}
一意な ID を生成するために、タイムスタンプとランダム文字列を組み合わせています。これにより、同じファイル名でも確実に区別できるのです。
進捗表示付きアップロード関数を実装します。
javascript/**
* 進捗表示付きファイルアップロード
* @param {File} file - アップロードするファイル
*/
function uploadFileWithProgress(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('timestamp', Date.now());
const xhr = new XMLHttpRequest();
// 対応する進捗バー要素を取得
const fileItem = fileList.lastElementChild;
const progressBar =
fileItem.querySelector('.progress-bar');
// アップロード進捗
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
if (percentComplete < 100) {
progressBar.textContent =
Math.round(percentComplete) + '%';
}
}
});
// アップロード完了
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
progressBar.style.width = '100%';
progressBar.style.backgroundColor = '#4CAF50';
progressBar.textContent = '完了';
console.log('アップロード成功:', xhr.responseText);
} else {
handleUploadError(xhr.status, progressBar);
}
});
// エラー処理
xhr.addEventListener('error', () => {
progressBar.style.backgroundColor = '#f44336';
progressBar.textContent = 'エラー';
showError('ネットワークエラーが発生しました。');
});
// タイムアウト処理(30秒)
xhr.addEventListener('timeout', () => {
progressBar.style.backgroundColor = '#ff9800';
progressBar.textContent = 'タイムアウト';
showError('アップロードがタイムアウトしました。');
});
xhr.timeout = 30000; // 30秒
xhr.open('POST', '/api/upload');
xhr.send(formData);
}
アップロードエラーを処理する関数を追加します。
javascript/**
* アップロードエラーの処理
* @param {number} status - HTTP ステータスコード
* @param {HTMLElement} progressBar - 進捗バー要素
*/
function handleUploadError(status, progressBar) {
progressBar.style.backgroundColor = '#f44336';
progressBar.textContent = 'エラー';
let errorMessage = 'アップロードに失敗しました。';
// エラーコード別のメッセージ
switch (status) {
case 400:
errorMessage = 'Error 400: 不正なリクエストです。';
break;
case 401:
errorMessage = 'Error 401: 認証が必要です。';
break;
case 413:
errorMessage =
'Error 413: ファイルサイズが大きすぎます。';
break;
case 415:
errorMessage =
'Error 415: サポートされていないファイル形式です。';
break;
case 500:
errorMessage =
'Error 500: サーバーエラーが発生しました。';
break;
case 503:
errorMessage =
'Error 503: サーバーが一時的に利用できません。';
break;
default:
errorMessage = `Error ${status}: アップロードに失敗しました。`;
}
showError(errorMessage);
}
HTTP ステータスコードに応じて適切なエラーメッセージを表示することで、ユーザーが問題を理解しやすくなります。エラーコードを含めることで、検索による解決策の発見も容易になるのです。
ユーティリティ関数を実装します。
javascript/**
* ファイルサイズを人間が読みやすい形式に変換
* @param {number} bytes - バイト数
* @returns {string} フォーマットされたサイズ
*/
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (
parseFloat((bytes / Math.pow(k, i)).toFixed(2)) +
' ' +
sizes[i]
);
}
/**
* エラーメッセージを表示
* @param {string} message - エラーメッセージ
*/
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
// ドロップ領域の後に挿入
dropArea.parentNode.insertBefore(
errorDiv,
dropArea.nextSibling
);
// 5秒後に自動削除
setTimeout(() => {
errorDiv.style.opacity = '0';
setTimeout(() => errorDiv.remove(), 300);
}, 5000);
}
サーバーサイド実装例(Node.js + Express)
クライアント側だけでなく、サーバー側の実装例も見ていきましょう。
javascript// 必要なモジュールのインポート
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
const port = 3000;
Multer の設定を行います。
javascript// Multer のストレージ設定
const storage = multer.diskStorage({
// 保存先ディレクトリ
destination: (req, file, cb) => {
const uploadDir = 'uploads/';
// ディレクトリが存在しない場合は作成
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
// ファイル名の設定
filename: (req, file, cb) => {
// 元のファイル名から拡張子を取得
const ext = path.extname(file.originalname);
// タイムスタンプ + ランダム文字列でユニークな名前を生成
const uniqueName =
Date.now() +
'-' +
Math.round(Math.random() * 1e9) +
ext;
cb(null, uniqueName);
},
});
ファイルフィルターを設定します。
javascript// ファイルフィルター(アップロード可能なファイルタイプの制限)
const fileFilter = (req, file, cb) => {
const allowedTypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(
new Error('許可されていないファイル形式です。'),
false
);
}
};
// Multer インスタンスの作成
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
});
アップロードエンドポイントを実装します。
javascript// 静的ファイルの配信
app.use(express.static('public'));
// アップロードエンドポイント
app.post(
'/api/upload',
upload.single('file'),
(req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'ファイルがアップロードされていません。',
});
}
// アップロード成功
res.status(200).json({
success: true,
message: 'ファイルのアップロードに成功しました。',
file: {
originalName: req.file.originalname,
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype,
path: req.file.path,
},
});
} catch (error) {
console.error('アップロードエラー:', error);
res.status(500).json({
success: false,
message: 'サーバーエラーが発生しました。',
});
}
}
);
エラーハンドリングミドルウェアを追加します。
javascript// Multer エラーハンドリング
app.use((error, req, res, next) => {
if (error instanceof multer.MulterError) {
// Multer 固有のエラー
if (error.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
success: false,
message:
'Error 413: ファイルサイズが 5MB を超えています。',
});
}
return res.status(400).json({
success: false,
message: `Error 400: ${error.message}`,
});
}
// その他のエラー
res.status(500).json({
success: false,
message: 'Error 500: サーバーエラーが発生しました。',
});
});
// サーバー起動
app.listen(port, () => {
console.log(
`サーバーが起動しました: http://localhost:${port}`
);
});
サーバー側でも必ずファイル検証を行うことが重要です。クライアント側の検証は簡単に回避できるため、セキュリティの観点からサーバー側でも二重にチェックする必要があるのです。
応用例:複数ファイルの一括アップロード
複数ファイルを一度にアップロードする機能を追加してみましょう。
javascript/**
* 複数ファイルの一括アップロード
* @param {FileList} files - アップロードするファイルリスト
*/
async function uploadMultipleFiles(files) {
const fileArray = Array.from(files);
const uploadPromises = [];
fileArray.forEach((file) => {
if (validateFile(file)) {
const progressBar = displayFilePreview(file);
const uploadPromise = uploadSingleFile(
file,
progressBar
);
uploadPromises.push(uploadPromise);
}
});
try {
// すべてのアップロードが完了するまで待機
const results = await Promise.all(uploadPromises);
console.log(
'すべてのアップロードが完了しました:',
results
);
showSuccess(
`${results.length} 件のファイルをアップロードしました。`
);
} catch (error) {
console.error('一括アップロードエラー:', error);
showError(
'一部のファイルのアップロードに失敗しました。'
);
}
}
Promise.all() を使うことで、複数のアップロードを並列実行し、すべての完了を待つことができます。これにより、効率的な一括アップロードが実現できるのです。
成功メッセージを表示する関数も追加しましょう。
javascript/**
* 成功メッセージを表示
* @param {string} message - 成功メッセージ
*/
function showSuccess(message) {
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
successDiv.style.cssText = `
padding: 12px 16px;
margin: 16px 0;
background-color: #e8f5e9;
color: #2e7d32;
border-radius: 6px;
border-left: 4px solid #4CAF50;
animation: slideIn 0.3s ease;
`;
dropArea.parentNode.insertBefore(
successDiv,
dropArea.nextSibling
);
setTimeout(() => {
successDiv.style.opacity = '0';
setTimeout(() => successDiv.remove(), 300);
}, 3000);
}
まとめ
JavaScript の Drag & Drop API を使ったファイルアップロード UI の実装方法について、基礎から実践まで解説してきました。
重要なポイントをおさらいしましょう。
| # | ポイント | 重要度 |
|---|---|---|
| 1 | preventDefault() でデフォルト動作を防止する | ★★★★★ |
| 2 | 4 つのイベント(dragenter, dragover, dragleave, drop)を正しく処理する | ★★★★★ |
| 3 | 視覚的フィードバックでユーザビリティを向上させる | ★★★★☆ |
| 4 | クライアントとサーバーの両方でファイル検証を行う | ★★★★★ |
| 5 | エラーコードを含めた適切なエラーハンドリングを実装する | ★★★★☆ |
| 6 | FileReader API でプレビュー機能を提供する | ★★★☆☆ |
| 7 | XMLHttpRequest で進捗表示を実装する | ★★★☆☆ |
Drag & Drop API は、モダンな Web アプリケーションには欠かせない機能です。今回ご紹介した実装パターンを基に、プロジェクトの要件に合わせてカスタマイズしていただければと思います。
特に、セキュリティ面では必ずサーバー側でもファイル検証を行い、適切なエラーハンドリングを実装することを忘れないでください。ユーザーにとって使いやすく、開発者にとってメンテナンスしやすいコードを心がけることが、長期的なプロジェクトの成功につながるのです。
この記事が、皆さんのファイルアップロード UI 実装の参考になれば幸いです。
関連リンク
articleJavaScript Drag & Drop API 完全攻略:ファイルアップロード UI を最速で作る
articleJavaScript structuredClone 徹底検証:JSON 方式や cloneDeep との速度・互換比較
articleJavaScript 時刻の落とし穴大全:タイムゾーン/DST/うるう秒の実務対策
articleJavaScript Web Animations API:滑らかに動く UI を設計するための基本と実践
articleJavaScript Service Worker 運用術:オフライン対応・更新・キャッシュ戦略の最適解
articleJavaScript パフォーマンス最適化大全:レイアウトスラッシングを潰す実践テク
articleVite プラグインフック対応表:Rollup → Vite マッピング早見表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleMySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articleMotion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来