概要
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.itemsをfor...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でスキップする