ファイルドロップゾーン

ドラッグ&ドロップとクリックでファイルを追加し、画像はサムネイル・他は拡張子バッジで一覧表示。個別削除やサイズ整形を備えたアップロードUIです。

#javascript#form#drag-drop

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<div class="sk-screen">
  <div class="dz-card">
    <div class="sk-head">
      <span class="sk-logo">🌸 Sakura</span>
      <span class="sk-badge">PHOTO CONTEST</span>
    </div>
    <h2 class="dz-title">推しショット投稿コンテスト</h2>
    <p class="sk-lead">ライブやイベントで撮影したお気に入りの一枚を応募してください(最大5枚)。</p>

    <!-- ドロップ可能エリア。クリックで写真選択も -->
    <label class="dropzone" id="dropzone">
      <input type="file" id="dz-input" multiple accept="image/*" hidden>
      <div class="dz-icon">📷</div>
      <p class="dz-text"><strong>写真をここにドラッグ&ドロップ</strong></p>
      <p class="dz-hint">またはクリックして選択(JPG / PNG)</p>
    </label>

    <!-- 応募写真の一覧 -->
    <ul class="dz-list" id="dz-list"></ul>
  </div>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(120% 80% at 50% -10%, #ffd1e0 0%, #ffe8f0 45%, #fbf3f6 100%);
  color: #6a4250;
}

.dz-card {
  width: min(420px, 94vw);
  padding: 22px 24px;
  background: #ffffff;
  border: 1px solid #f6d8e3;
  border-radius: 18px;
  box-shadow: 0 24px 56px -24px rgba(214, 120, 160, 0.55);
}

.sk-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.sk-logo { font-size: 0.95rem; font-weight: 800; color: #e87fa5; letter-spacing: 0.04em; }
.sk-badge {
  font-size: 0.6rem; font-weight: 700; letter-spacing: 0.1em;
  color: #d96a92; padding: 3px 8px;
  background: #ffe3ee; border-radius: 99px;
}

.dz-title { margin: 0 0 4px; font-size: 1.06rem; font-weight: 800; color: #5a3344; }
.sk-lead { margin: 0 0 14px; font-size: 0.74rem; line-height: 1.5; color: #a87f90; }

.dropzone {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 24px 18px; text-align: center;
  background: #fff7fb;
  border: 2px dashed #f1c0d2;
  border-radius: 14px; cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease;
}
.dropzone:hover { border-color: #ec86ab; }
/* ドラッグ侵入時の強調 */
.dropzone.is-dragover {
  border-color: #e8709a;
  background: #ffe9f1;
  transform: scale(1.02);
}

.dz-icon {
  width: 46px; height: 46px;
  display: grid; place-items: center; font-size: 1.3rem; color: #fff;
  background: linear-gradient(135deg, #ff9cc0, #e8709a);
  border-radius: 50%;
  box-shadow: 0 10px 22px -8px rgba(232, 112, 154, 0.7);
}
.dz-text { margin: 7px 0 0; font-size: 0.9rem; }
.dz-text strong { color: #5a3344; }
.dz-hint { margin: 0; font-size: 0.72rem; color: #b58da0; }

/* 応募写真一覧 */
.dz-list { list-style: none; margin: 13px 0 0; padding: 0; display: grid; gap: 8px; }
.dz-item {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 11px;
  background: #fff7fb;
  border: 1px solid #f3d2df;
  border-radius: 10px;
  font-size: 0.8rem;
  animation: pop 0.25s ease;
}
.dz-thumb {
  width: 34px; height: 34px; flex: none;
  border-radius: 7px; object-fit: cover;
  background: linear-gradient(135deg, #ff9cc0, #e8709a);
  display: grid; place-items: center;
  color: #fff; font-size: 0.68rem; font-weight: 700;
}
.dz-meta { flex: 1; min-width: 0; }
.dz-name { display: block; color: #5a3344; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dz-size { color: #b58da0; font-size: 0.72rem; }
.dz-remove {
  flex: none; width: 24px; height: 24px;
  color: #c69aab; background: transparent;
  border: none; border-radius: 6px; cursor: pointer;
  font-size: 1rem; line-height: 1;
}
.dz-remove:hover { background: #ffe3ee; color: #e35d7c; }

@keyframes pop {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .dropzone { transition: none; }
  .dz-item { animation: none; }
}
JavaScript
const zone = document.getElementById("dropzone");
const fileInput = document.getElementById("dz-input");
const list = document.getElementById("dz-list");
const store = new Map(); // 表示中ファイルを一意に管理
const MAX = 5; // 応募上限枚数

// バイト数を読みやすく整形
function fmtSize(bytes) {
  if (bytes < 1024) return bytes + " B";
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
  return (bytes / 1024 / 1024).toFixed(1) + " MB";
}

// 拡張子バッジ用のラベル
function badge(file) {
  const ext = (file.name.split(".").pop() || "?").toUpperCase();
  return ext.slice(0, 3);
}

// 1ファイルを一覧へ追加
function addFile(file) {
  if (store.size >= MAX) return; // 上限を超えたら無視
  const key = file.name + file.size;
  if (store.has(key)) return; // 重複は無視
  store.set(key, file);

  const li = document.createElement("li");
  li.className = "dz-item";

  const thumb = document.createElement("div");
  thumb.className = "dz-thumb";

  // 画像はサムネイル、それ以外は拡張子バッジ
  if (file.type.startsWith("image/")) {
    const img = document.createElement("img");
    img.className = "dz-thumb";
    img.alt = file.name;
    img.src = URL.createObjectURL(file);
    img.onload = () => URL.revokeObjectURL(img.src); // メモリ解放
    li.appendChild(img);
  } else {
    thumb.textContent = badge(file);
    li.appendChild(thumb);
  }

  const meta = document.createElement("div");
  meta.className = "dz-meta";
  meta.innerHTML = `<span class="dz-name"></span><span class="dz-size"></span>`;
  meta.querySelector(".dz-name").textContent = file.name;
  meta.querySelector(".dz-size").textContent = fmtSize(file.size);
  li.appendChild(meta);

  const remove = document.createElement("button");
  remove.className = "dz-remove";
  remove.type = "button";
  remove.textContent = "×";
  remove.setAttribute("aria-label", "削除");
  remove.addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
    store.delete(key);
    li.remove();
  });
  li.appendChild(remove);

  list.appendChild(li);
}

function handleFiles(files) {
  [...files].forEach(addFile);
}

if (zone && fileInput && list) {
  // クリックでファイル選択
  fileInput.addEventListener("change", () => handleFiles(fileInput.files));

  // ドラッグ&ドロップ
  ["dragenter", "dragover"].forEach((ev) =>
    zone.addEventListener(ev, (e) => {
      e.preventDefault();
      zone.classList.add("is-dragover");
    })
  );
  ["dragleave", "drop"].forEach((ev) =>
    zone.addEventListener(ev, (e) => {
      e.preventDefault();
      zone.classList.remove("is-dragover");
    })
  );
  zone.addEventListener("drop", (e) => {
    if (e.dataTransfer) handleFiles(e.dataTransfer.files);
  });
}

コード

HTML
<div class="stage">
  <div class="dz-card">
    <h2 class="dz-title">ファイルをアップロード</h2>

    <!-- ドロップ可能エリア。クリックでファイル選択も -->
    <label class="dropzone" id="dropzone">
      <input type="file" id="dz-input" multiple accept="image/*,.pdf,.zip" hidden>
      <div class="dz-icon">⬆</div>
      <p class="dz-text"><strong>ここにドラッグ&ドロップ</strong></p>
      <p class="dz-hint">またはクリックして選択 (画像 / PDF / ZIP)</p>
    </label>

    <!-- 追加されたファイル一覧 -->
    <ul class="dz-list" id="dz-list"></ul>
  </div>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: linear-gradient(135deg, #0f2027, #203a43 55%, #2c5364);
  color: #e5e7eb;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.dz-card {
  width: min(420px, 94vw);
  padding: 24px 22px;
  background: rgba(17, 25, 40, 0.9);
  border: 1px solid #2a3a4a;
  border-radius: 16px;
  box-shadow: 0 24px 56px -24px rgba(0, 0, 0, 0.7);
}

.dz-title { margin: 0 0 16px; font-size: 1.1rem; letter-spacing: 0.04em; }

.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 26px 18px;
  text-align: center;
  background: rgba(56, 189, 248, 0.04);
  border: 2px dashed #3b5468;
  border-radius: 14px;
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease;
}
.dropzone:hover { border-color: #38bdf8; }
/* ドラッグ侵入時の強調 */
.dropzone.is-dragover {
  border-color: #38bdf8;
  background: rgba(56, 189, 248, 0.14);
  transform: scale(1.02);
}

.dz-icon {
  width: 48px; height: 48px;
  display: grid; place-items: center;
  font-size: 1.4rem;
  color: #fff;
  background: linear-gradient(135deg, #0ea5e9, #6366f1);
  border-radius: 50%;
  box-shadow: 0 10px 22px -8px rgba(14, 165, 233, 0.7);
}
.dz-text { margin: 8px 0 0; font-size: 0.92rem; }
.dz-text strong { color: #f1f5f9; }
.dz-hint { margin: 0; font-size: 0.74rem; color: #7d8ca3; }

/* ファイル一覧 */
.dz-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }
.dz-item {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 11px;
  background: #16203250;
  border: 1px solid #2a3a4a;
  border-radius: 10px;
  font-size: 0.8rem;
  animation: pop 0.25s ease;
}
.dz-thumb {
  width: 34px; height: 34px; flex: none;
  border-radius: 7px;
  object-fit: cover;
  background: #0ea5e9;
  display: grid; place-items: center;
  color: #fff; font-size: 0.7rem; font-weight: 700;
}
.dz-meta { flex: 1; min-width: 0; }
.dz-name { display: block; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dz-size { color: #7d8ca3; font-size: 0.72rem; }
.dz-remove {
  flex: none;
  width: 24px; height: 24px;
  color: #94a3b8;
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}
.dz-remove:hover { background: #2a3a4a; color: #f87171; }

@keyframes pop {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .dropzone { transition: none; }
  .dz-item { animation: none; }
}
JavaScript
const zone = document.getElementById("dropzone");
const fileInput = document.getElementById("dz-input");
const list = document.getElementById("dz-list");
const store = new Map(); // 表示中ファイルを一意に管理

// バイト数を読みやすく整形
function fmtSize(bytes) {
  if (bytes < 1024) return bytes + " B";
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
  return (bytes / 1024 / 1024).toFixed(1) + " MB";
}

// 拡張子バッジ用のラベル
function badge(file) {
  const ext = (file.name.split(".").pop() || "?").toUpperCase();
  return ext.slice(0, 3);
}

// 1ファイルを一覧へ追加
function addFile(file) {
  const key = file.name + file.size;
  if (store.has(key)) return; // 重複は無視
  store.set(key, file);

  const li = document.createElement("li");
  li.className = "dz-item";

  const thumb = document.createElement("div");
  thumb.className = "dz-thumb";

  // 画像はサムネイル、それ以外は拡張子バッジ
  if (file.type.startsWith("image/")) {
    const img = document.createElement("img");
    img.className = "dz-thumb";
    img.alt = file.name;
    img.src = URL.createObjectURL(file);
    img.onload = () => URL.revokeObjectURL(img.src); // メモリ解放
    thumb.replaceWith(img);
    li.appendChild(img);
  } else {
    thumb.textContent = badge(file);
    li.appendChild(thumb);
  }

  const meta = document.createElement("div");
  meta.className = "dz-meta";
  meta.innerHTML = `<span class="dz-name"></span><span class="dz-size"></span>`;
  meta.querySelector(".dz-name").textContent = file.name;
  meta.querySelector(".dz-size").textContent = fmtSize(file.size);
  li.appendChild(meta);

  const remove = document.createElement("button");
  remove.className = "dz-remove";
  remove.type = "button";
  remove.textContent = "×";
  remove.setAttribute("aria-label", "削除");
  remove.addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
    store.delete(key);
    li.remove();
  });
  li.appendChild(remove);

  list.appendChild(li);
}

function handleFiles(files) {
  [...files].forEach(addFile);
}

if (zone && fileInput && list) {
  // クリックでファイル選択(labelなので自動だが、安全に明示)
  fileInput.addEventListener("change", () => handleFiles(fileInput.files));

  // ドラッグ&ドロップ
  ["dragenter", "dragover"].forEach((ev) =>
    zone.addEventListener(ev, (e) => {
      e.preventDefault();
      zone.classList.add("is-dragover");
    })
  );
  ["dragleave", "drop"].forEach((ev) =>
    zone.addEventListener(ev, (e) => {
      e.preventDefault();
      zone.classList.remove("is-dragover");
    })
  );
  zone.addEventListener("drop", (e) => {
    if (e.dataTransfer) handleFiles(e.dataTransfer.files);
  });
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ファイルドロップゾーン」の効果を追加してください。

# 追加してほしい効果
ファイルドロップゾーン(フォーム & 入力)
ドラッグ&ドロップとクリックでファイルを追加し、画像はサムネイル・他は拡張子バッジで一覧表示。個別削除やサイズ整形を備えたアップロードUIです。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="stage">
  <div class="dz-card">
    <h2 class="dz-title">ファイルをアップロード</h2>

    <!-- ドロップ可能エリア。クリックでファイル選択も -->
    <label class="dropzone" id="dropzone">
      <input type="file" id="dz-input" multiple accept="image/*,.pdf,.zip" hidden>
      <div class="dz-icon">⬆</div>
      <p class="dz-text"><strong>ここにドラッグ&ドロップ</strong></p>
      <p class="dz-hint">またはクリックして選択 (画像 / PDF / ZIP)</p>
    </label>

    <!-- 追加されたファイル一覧 -->
    <ul class="dz-list" id="dz-list"></ul>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: linear-gradient(135deg, #0f2027, #203a43 55%, #2c5364);
  color: #e5e7eb;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.dz-card {
  width: min(420px, 94vw);
  padding: 24px 22px;
  background: rgba(17, 25, 40, 0.9);
  border: 1px solid #2a3a4a;
  border-radius: 16px;
  box-shadow: 0 24px 56px -24px rgba(0, 0, 0, 0.7);
}

.dz-title { margin: 0 0 16px; font-size: 1.1rem; letter-spacing: 0.04em; }

.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 26px 18px;
  text-align: center;
  background: rgba(56, 189, 248, 0.04);
  border: 2px dashed #3b5468;
  border-radius: 14px;
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease;
}
.dropzone:hover { border-color: #38bdf8; }
/* ドラッグ侵入時の強調 */
.dropzone.is-dragover {
  border-color: #38bdf8;
  background: rgba(56, 189, 248, 0.14);
  transform: scale(1.02);
}

.dz-icon {
  width: 48px; height: 48px;
  display: grid; place-items: center;
  font-size: 1.4rem;
  color: #fff;
  background: linear-gradient(135deg, #0ea5e9, #6366f1);
  border-radius: 50%;
  box-shadow: 0 10px 22px -8px rgba(14, 165, 233, 0.7);
}
.dz-text { margin: 8px 0 0; font-size: 0.92rem; }
.dz-text strong { color: #f1f5f9; }
.dz-hint { margin: 0; font-size: 0.74rem; color: #7d8ca3; }

/* ファイル一覧 */
.dz-list { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }
.dz-item {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 11px;
  background: #16203250;
  border: 1px solid #2a3a4a;
  border-radius: 10px;
  font-size: 0.8rem;
  animation: pop 0.25s ease;
}
.dz-thumb {
  width: 34px; height: 34px; flex: none;
  border-radius: 7px;
  object-fit: cover;
  background: #0ea5e9;
  display: grid; place-items: center;
  color: #fff; font-size: 0.7rem; font-weight: 700;
}
.dz-meta { flex: 1; min-width: 0; }
.dz-name { display: block; color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dz-size { color: #7d8ca3; font-size: 0.72rem; }
.dz-remove {
  flex: none;
  width: 24px; height: 24px;
  color: #94a3b8;
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
}
.dz-remove:hover { background: #2a3a4a; color: #f87171; }

@keyframes pop {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .dropzone { transition: none; }
  .dz-item { animation: none; }
}

【JavaScript】
const zone = document.getElementById("dropzone");
const fileInput = document.getElementById("dz-input");
const list = document.getElementById("dz-list");
const store = new Map(); // 表示中ファイルを一意に管理

// バイト数を読みやすく整形
function fmtSize(bytes) {
  if (bytes < 1024) return bytes + " B";
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
  return (bytes / 1024 / 1024).toFixed(1) + " MB";
}

// 拡張子バッジ用のラベル
function badge(file) {
  const ext = (file.name.split(".").pop() || "?").toUpperCase();
  return ext.slice(0, 3);
}

// 1ファイルを一覧へ追加
function addFile(file) {
  const key = file.name + file.size;
  if (store.has(key)) return; // 重複は無視
  store.set(key, file);

  const li = document.createElement("li");
  li.className = "dz-item";

  const thumb = document.createElement("div");
  thumb.className = "dz-thumb";

  // 画像はサムネイル、それ以外は拡張子バッジ
  if (file.type.startsWith("image/")) {
    const img = document.createElement("img");
    img.className = "dz-thumb";
    img.alt = file.name;
    img.src = URL.createObjectURL(file);
    img.onload = () => URL.revokeObjectURL(img.src); // メモリ解放
    thumb.replaceWith(img);
    li.appendChild(img);
  } else {
    thumb.textContent = badge(file);
    li.appendChild(thumb);
  }

  const meta = document.createElement("div");
  meta.className = "dz-meta";
  meta.innerHTML = `<span class="dz-name"></span><span class="dz-size"></span>`;
  meta.querySelector(".dz-name").textContent = file.name;
  meta.querySelector(".dz-size").textContent = fmtSize(file.size);
  li.appendChild(meta);

  const remove = document.createElement("button");
  remove.className = "dz-remove";
  remove.type = "button";
  remove.textContent = "×";
  remove.setAttribute("aria-label", "削除");
  remove.addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
    store.delete(key);
    li.remove();
  });
  li.appendChild(remove);

  list.appendChild(li);
}

function handleFiles(files) {
  [...files].forEach(addFile);
}

if (zone && fileInput && list) {
  // クリックでファイル選択(labelなので自動だが、安全に明示)
  fileInput.addEventListener("change", () => handleFiles(fileInput.files));

  // ドラッグ&ドロップ
  ["dragenter", "dragover"].forEach((ev) =>
    zone.addEventListener(ev, (e) => {
      e.preventDefault();
      zone.classList.add("is-dragover");
    })
  );
  ["dragleave", "drop"].forEach((ev) =>
    zone.addEventListener(ev, (e) => {
      e.preventDefault();
      zone.classList.remove("is-dragover");
    })
  );
  zone.addEventListener("drop", (e) => {
    if (e.dataTransfer) handleFiles(e.dataTransfer.files);
  });
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。