レビュー待ち·難易度: 中級·更新: 2026-04-18

React でドラッグ&ドロップ対応のファイル選択UIを実装する

DragEvent API を使いドラッグ&ドロップでファイルを受け付けるクライアント UI を実装する例。ドロップゾーンの視覚フィードバックとクリック選択の併用パターンも示す。

nextjsfile-uploadui-component

対応バージョン

nextjs 15react 19

前提環境

React の useState と useRef の基本を理解していること

概要

onDragOver / onDrop イベントを使い、ファイルをドラッグ&ドロップで受け付けるドロップゾーン UI を実装する。ドラッグ中の視覚フィードバックとクリックによるファイル選択を併用し、ライブラリ不要で構築する。

インストール

# 追加インストールは不要

実装

// components/FileDropZone.tsx
"use client";

import { useRef, useState } from "react";

type Props = {
  accept?: string;
  onFilesSelected: (files: File[]) => void;
};

export function FileDropZone({ accept = "*", onFilesSelected }: Props) {
  const [isDragging, setIsDragging] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  function handleDragOver(e: React.DragEvent) {
    e.preventDefault();
    setIsDragging(true);
  }

  function handleDragLeave(e: React.DragEvent) {
    // relatedTarget が子要素への移動のとき誤発火しないようにする
    if (e.currentTarget.contains(e.relatedTarget as Node)) return;
    setIsDragging(false);
  }

  function handleDrop(e: React.DragEvent) {
    e.preventDefault();
    setIsDragging(false);
    const files = Array.from(e.dataTransfer.files);
    if (files.length > 0) onFilesSelected(files);
  }

  function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
    const files = Array.from(e.target.files ?? []);
    if (files.length > 0) onFilesSelected(files);
  }

  return (
    <div
      onDragOver={handleDragOver}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
      onClick={() => inputRef.current?.click()}
      className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors
        ${isDragging
          ? "border-blue-500 bg-blue-50 text-blue-600"
          : "border-gray-300 bg-gray-50 text-gray-500 hover:border-gray-400"
        }`}
    >
      <p className="text-sm font-medium">
        {isDragging ? "ここにドロップ" : "ファイルをドラッグ&ドロップ"}
      </p>
      <p className="mt-1 text-xs">または クリックして選択</p>
      <input
        ref={inputRef}
        type="file"
        accept={accept}
        multiple
        className="hidden"
        onChange={handleInputChange}
        onClick={(e) => e.stopPropagation()}
      />
    </div>
  );
}

ページでの使用例

// app/page.tsx
"use client";

import { useState } from "react";
import { FileDropZone } from "@/components/FileDropZone";

export default function Page() {
  const [files, setFiles] = useState<File[]>([]);

  function handleFilesSelected(selected: File[]) {
    setFiles((prev) => [...prev, ...selected]);
  }

  return (
    <main className="mx-auto max-w-lg p-8">
      <h1 className="mb-6 text-2xl font-bold">ファイルアップロード</h1>
      <FileDropZone accept="image/*" onFilesSelected={handleFilesSelected} />

      {files.length > 0 && (
        <ul className="mt-4 space-y-2">
          {files.map((file, i) => (
            <li key={i} className="flex items-center justify-between rounded border p-3 text-sm">
              <span className="truncate text-gray-700">{file.name}</span>
              <span className="ml-4 shrink-0 text-xs text-gray-400">
                {(file.size / 1024).toFixed(1)} KB
              </span>
            </li>
          ))}
        </ul>
      )}
    </main>
  );
}

ポイント

  • onDragOvere.preventDefault() を呼ばないとブラウザのデフォルト動作(ファイルを開く)が発生する。必須
  • onDragLeave は子要素へのマウス移動でも発火するため、e.currentTarget.contains(e.relatedTarget) で内部移動を除外するとドロップゾーンが点滅しなくなる
  • e.dataTransfer.filesFileList なので Array.from() で配列に変換する
  • input[type="file"] を hidden で持ち、クリック時に inputRef.current.click() でプログラム的に開くことで D&D とクリック選択を両立できる
  • react-hook-form-file-upload との使い分け: フォームバリデーションが必要なら RHF 版、ファイル受け付け UI のみ独立して使いたい場合はこのパターン

注意点

react-hook-form-file-upload は RHF 統合のファイル選択 UI。nextjs-file-upload-api はサーバー受信処理。これは DragEvent API を直接使ったドラッグ&ドロップ UI(ライブラリ不要)。

関連サンプル