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

React でクリップボード貼り付け(Ctrl+V)から画像をアップロードする

ClipboardEvent の clipboardData.items を走査して image/* を抽出し、URL.createObjectURL でプレビュー表示してからサーバーへアップロードする UI パターン。

nextjsfile-uploadui-component

対応バージョン

nextjs 15react 19

前提環境

React の useState・useRef・イベントハンドリングの基本を理解していること

概要

paste イベントで ClipboardEvent.clipboardData.items を走査し、image/* タイプのアイテムを File として取得する。URL.createObjectURL でプレビューを即時表示し、確認後にサーバーへ送信するパターン。スクリーンショットや他サイトからコピーした画像を手軽に貼り付けられる UI に使用する。

インストール

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

実装

貼り付けエリアコンポーネント

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

import { useRef, useState } from "react";

type UploadState = "idle" | "uploading" | "done" | "error";

export default function PasteImageUploader() {
  const [preview, setPreview] = useState<string | null>(null);
  const [file, setFile] = useState<File | null>(null);
  const [uploadState, setUploadState] = useState<UploadState>("idle");
  const areaRef = useRef<HTMLDivElement>(null);

  function handlePaste(e: React.ClipboardEvent<HTMLDivElement>) {
    const items = e.clipboardData.items;

    for (const item of items) {
      if (!item.type.startsWith("image/")) continue;

      const blob = item.getAsFile();
      if (!blob) continue;

      // 既存プレビューの Object URL を解放してメモリリークを防ぐ
      if (preview) URL.revokeObjectURL(preview);

      setPreview(URL.createObjectURL(blob));
      setFile(blob);
      setUploadState("idle");
      break;
    }
  }

  async function handleUpload() {
    if (!file) return;

    setUploadState("uploading");

    const fd = new FormData();
    fd.append("file", file, file.name || "paste.png");

    try {
      const res = await fetch("/api/upload", { method: "POST", body: fd });
      if (!res.ok) throw new Error("upload failed");
      setUploadState("done");
    } catch {
      setUploadState("error");
    }
  }

  function handleClear() {
    if (preview) URL.revokeObjectURL(preview);
    setPreview(null);
    setFile(null);
    setUploadState("idle");
    areaRef.current?.focus();
  }

  return (
    <div className="space-y-4">
      {/* 貼り付けエリア */}
      <div
        ref={areaRef}
        onPaste={handlePaste}
        tabIndex={0}
        className="flex min-h-40 cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-6 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
        aria-label="画像を貼り付けるエリア(Ctrl+V)"
      >
        {preview ? (
          <img
            src={preview}
            alt="貼り付けた画像のプレビュー"
            className="max-h-64 max-w-full rounded object-contain"
          />
        ) : (
          <p className="select-none text-sm text-gray-500">
            ここをクリックしてから <kbd className="rounded border px-1 py-0.5 text-xs">Ctrl+V</kbd> で画像を貼り付け
          </p>
        )}
      </div>

      {/* アクションボタン */}
      {file && (
        <div className="flex items-center gap-3">
          <button
            onClick={handleUpload}
            disabled={uploadState === "uploading" || uploadState === "done"}
            className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
          >
            {uploadState === "uploading"
              ? "アップロード中…"
              : uploadState === "done"
                ? "完了"
                : "アップロード"}
          </button>
          <button
            onClick={handleClear}
            className="rounded border px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
          >
            クリア
          </button>
          {uploadState === "error" && (
            <p className="text-sm text-red-500">アップロードに失敗しました</p>
          )}
        </div>
      )}
    </div>
  );
}

アップロード API

// app/api/upload/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const fd = await request.formData();
  const file = fd.get("file");

  if (!(file instanceof File)) {
    return NextResponse.json({ error: "ファイルがありません" }, { status: 400 });
  }

  // 実際のアプリでは S3 や Cloudflare R2 などへ保存する
  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);

  console.log(`受け取ったファイル: ${file.name}, ${buffer.byteLength} bytes`);

  return NextResponse.json({
    message: "アップロード成功",
    name: file.name,
    size: buffer.byteLength,
  });
}

ページへの組み込み

// app/page.tsx
import PasteImageUploader from "@/components/PasteImageUploader";

export default function Page() {
  return (
    <main className="mx-auto max-w-xl p-8">
      <h1 className="mb-6 text-2xl font-bold">画像を貼り付けてアップロード</h1>
      <PasteImageUploader />
    </main>
  );
}

ポイント

  • onPaste ハンドラ内で e.clipboardData.itemsfor...of で走査し、item.type.startsWith("image/") で画像のみを抽出する。item.getAsFile()File オブジェクトに変換できる
  • URL.createObjectURL(blob) でブラウザ内オブジェクト URL を生成してプレビューに使用する。次の貼り付け時に以前の URL を URL.revokeObjectURL で解放し、メモリリークを防ぐ
  • 貼り付けエリアは tabIndex={0} で focusable にし、クリック後に Ctrl+V でイベントを受け取れるようにする。focus:border-blue-500 のフォーカスリングでアクティブ状態を視覚的に示す
  • FormData.append(file, file.name || "paste.png") の第 3 引数でファイル名を付与する。クリップボード由来のファイルは name が空になることがあるためフォールバックを設定する
  • スクリーンショット(PNG)や他サイトからコピーした画像(JPEG / WebP)など image/* に一致するすべての形式を受け付ける。PDF やテキストなど非画像アイテムは continue でスキップする

注意点

nextjs-file-upload-api はファイル入力やドラッグ&ドロップからのアップロード。これはクリップボード貼り付け(Ctrl+V)経由で画像を取得する切り口に特化。ClipboardEvent.clipboardData.items の走査・image/* フィルタ・URL.createObjectURL によるプレビューを示す。

関連サンプル