Clipboard Events
Intercept copy, cut, and paste operations for custom clipboard behavior.
Search across all documentation pages
Intercept copy, cut, and paste operations for custom clipboard behavior.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Event | Fires When | Bubbles | Cancelable | Key Property |
|---|---|---|---|---|
onCopy | User copies selection (Ctrl+C / Cmd+C, or context menu) | Yes | Yes | clipboardData: DataTransfer |
onCut | User cuts selection (Ctrl+X / Cmd+X, or context menu) | Yes | Yes | clipboardData: DataTransfer |
onPaste | User pastes content (Ctrl+V / Cmd+V, or context menu) | Yes | Yes | clipboardData: DataTransfer |
All clipboard events provide a
clipboardDataproperty of typeDataTransferthat lets you read or write data in multiple formats (text/plain, text/html, custom MIME types).
Quick-reference recipe card -- copy-paste ready.
// Add attribution when copying text
function AttributedContent({ children }: { children: React.ReactNode }) {
const handleCopy = (e: React.ClipboardEvent) => {
const selection = window.getSelection()?.toString() ?? "";
const attributed = `${selection}\n\n-- Source: mysite.com`;
e.clipboardData.setData("text/plain", attributed);
e.preventDefault(); // Required to use custom clipboard data
};
return <div onCopy={handleCopy}>{children}</div>;
}
// Sanitize pasted input
function SanitizedInput() {
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
const sanitized = text.replace(/[<>]/g, ""); // strip angle brackets
document.execCommand("insertText", false, sanitized);
};
return <input onPaste={handlePaste} placeholder="Paste here (sanitized)" />;
}When to reach for this: You need to add attribution to copied text, sanitize pasted input, handle image paste for uploads, or implement a custom copy button.
// components/CopyableCodeBlock.tsx
"use client";
import { useState, useRef } from "react";
export function CopyableCodeBlock({ code }: { code: string }) {
const [copied, setCopied] = useState(false);
const preRef = useRef<HTMLPreElement>(null);
const handleCopyClick = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for insecure contexts
const textarea = document.createElement("textarea");
textarea.value = code;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="relative group">
<pre
ref={preRef}
className="bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto"
>
<code>{code}</code>
</pre>
<button
onClick={handleCopyClick}
className="absolute top-2 right-2 px-2 py-1 text-xs bg-gray-700 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity"
>
{copied ? "Copied!" : "Copy"}
</button>
</div>
);
}// components/PasteUploadArea.tsx
"use client";
import { useState } from "react";
type PastedFile = {
name: string;
type: string;
size: number;
preview: string;
};
export function PasteUploadArea() {
const [files, setFiles] = useState<PastedFile[]>([]);
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
const items = e.clipboardData.items;
const newFiles: PastedFile[] = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.kind === "file") {
const file = item.getAsFile();
if (!file) continue;
const preview = URL.createObjectURL(file);
newFiles.push({
name: file.name || `pasted-${Date.now()}.${file.type.split("/")[1]}`,
type: file.type,
size: file.size,
preview,
});
}
}
if (newFiles.length > 0) {
e.preventDefault();
setFiles((prev) => [...prev, ...newFiles]);
}
};
return (
<div
onPaste={handlePaste}
tabIndex={0}
className="border-2 border-dashed border-gray-400 rounded p-8 text-center focus:border-blue-500 focus:outline-none"
>
<p className="text-gray-600 mb-4">
Click here and paste an image (Ctrl+V / Cmd+V)
</p>
{files.length > 0 && (
<div className="grid grid-cols-3 gap-4 mt-4">
{files.map((f, i) => (
<div key={i} className="border rounded p-2">
{f.type.startsWith("image/") ? (
<img
src={f.preview}
alt={f.name}
className="w-full h-32 object-cover rounded"
/>
) : (
<p className="text-sm">{f.name}</p>
)}
<p className="text-xs text-gray-500 mt-1">
{f.type} ({(f.size / 1024).toFixed(1)} KB)
</p>
</div>
))}
</div>
)}
</div>
);
}What this demonstrates:
navigator.clipboard.writeText for modern async copy with a fallback for insecure contextsclipboardData.itemstabIndex={0} so it can receive focus and clipboard eventsClipboardEvent. React normalizes them across browsers.clipboardData property is a DataTransfer object that provides getData(format), setData(format, data), and items for file access.e.preventDefault() is required when using setData -- without it, the browser overwrites your custom data with the default copy behavior.<div> must have tabIndex to be focusable and receive paste events.Copy plain text to clipboard (programmatic, no event):
function CopyButton({ text }: { text: string }) {
const handleClick = async () => {
await navigator.clipboard.writeText(text);
};
return <button onClick={handleClick}>Copy</button>;
}Paste image handling with type checking:
function ImagePasteHandler() {
const handlePaste = (e: React.ClipboardEvent) => {
const items = Array.from(e.clipboardData.items);
const imageItem = items.find((item) => item.type.startsWith("image/"));
if (!imageItem) return;
e.preventDefault();
const file = imageItem.getAsFile();
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const dataUrl = event.target?.result as string;
// Use dataUrl for preview or upload
};
reader.readAsDataURL(file);
};
return <div onPaste={handlePaste} tabIndex={0}>Paste an image here</div>;
}Custom copy formatting (HTML + plain text):
function RichCopyTable({ rows }: { rows: string[][] }) {
const handleCopy = (e: React.ClipboardEvent) => {
e.preventDefault();
// Set both plain text and HTML versions
const plainText = rows.map((row) => row.join("\t")).join("\n");
const html = `<table>${rows
.map((row) => `<tr>${row.map((c) => `<td>${c}</td>`).join("")}</tr>`)
.join("")}</table>`;
e.clipboardData.setData("text/plain", plainText);
e.clipboardData.setData("text/html", html);
};
return (
<table onCopy={handleCopy}>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j} className="border px-2 py-1">{cell}</td>
))}
</tr>
))}
</tbody>
</table>
);
}Preventing paste in confirm fields:
function ConfirmEmailField() {
return (
<div>
<label>Email</label>
<input type="email" name="email" />
<label>Confirm Email</label>
<input
type="email"
name="confirmEmail"
onPaste={(e) => {
e.preventDefault();
// Optionally show a tooltip: "Please type your email again"
}}
/>
</div>
);
}Using the Clipboard API (navigator.clipboard) for reading:
function ClipboardReader() {
const [content, setContent] = useState("");
const readClipboard = async () => {
try {
// Requires user gesture and permissions
const text = await navigator.clipboard.readText();
setContent(text);
} catch (err) {
console.error("Clipboard read failed:", err);
}
};
return (
<div>
<button onClick={readClipboard}>Read Clipboard</button>
{content && <pre className="mt-2 p-2 bg-gray-100 rounded">{content}</pre>}
</div>
);
}// ClipboardEvent generic specifies the element
const handleCopy = (e: React.ClipboardEvent<HTMLDivElement>) => {
e.clipboardData; // DataTransfer (always available on clipboard events)
e.currentTarget; // HTMLDivElement
};
// clipboardData methods are fully typed
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
const text: string = e.clipboardData.getData("text/plain");
const html: string = e.clipboardData.getData("text/html");
// items is a DataTransferItemList
const items: DataTransferItemList = e.clipboardData.items;
// Iterating items
for (let i = 0; i < items.length; i++) {
const item: DataTransferItem = items[i];
item.kind; // "string" | "file"
item.type; // MIME type string
if (item.kind === "file") {
const file: File | null = item.getAsFile();
}
}
};
// Using the shorthand handler type
const onCut: React.ClipboardEventHandler<HTMLTextAreaElement> = (e) => {
// e is React.ClipboardEvent<HTMLTextAreaElement>
};
// navigator.clipboard API types (built into lib.dom.d.ts)
async function writeToClipboard(text: string): Promise<void> {
await navigator.clipboard.writeText(text);
}
async function readFromClipboard(): Promise<string> {
return navigator.clipboard.readText();
}e.preventDefault() is required when using setData -- If you call e.clipboardData.setData() without e.preventDefault(), the browser replaces your custom data with the default copy. Fix: Always call e.preventDefault() before or after setData.
clipboardData is only available during the event -- The DataTransfer object is cleared after the event handler completes. You cannot store the event and read clipboardData asynchronously. Fix: Extract the data you need synchronously within the handler and store it in a variable or state.
navigator.clipboard requires HTTPS and user activation -- The async Clipboard API only works in secure contexts (HTTPS or localhost) and requires a recent user gesture (click, keypress). Fix: Provide a fallback using document.execCommand("copy") for HTTP contexts, and always call clipboard methods from event handlers.
navigator.clipboard.readText() triggers a browser permission prompt -- Unlike writing, reading the clipboard requires explicit user permission. This prompt can confuse users. Fix: Use onPaste event handlers to read pasted content instead of proactively reading the clipboard, unless your UX specifically requires it.
Pasted files have empty or generic names -- When a user pastes a screenshot, file.name is often empty or something like image.png. Fix: Generate meaningful names using timestamps or context, like pasted-${Date.now()}.png.
document.execCommand("copy") is deprecated -- While still widely supported, it may be removed in future browsers. Fix: Use navigator.clipboard.writeText() as the primary method with execCommand as a fallback only.
Non-input elements cannot receive paste events without focus -- A <div> will not fire onPaste unless it is focused. Fix: Add tabIndex={0} or tabIndex={-1} to make the container focusable, and instruct users to click/focus the area first.
| Alternative | Use When | Don't Use When |
|---|---|---|
navigator.clipboard API | You need programmatic read/write without a clipboard event | You are intercepting user-initiated copy/paste |
document.execCommand("copy") | You need clipboard access in insecure (HTTP) contexts | You can use the modern Clipboard API |
Third-party libraries (e.g., clipboard.js) | You need cross-browser clipboard support with minimal code | The native API and React events cover your needs |
onKeyDown Ctrl+C detection | You need to know when the user attempts to copy without intercepting it | You need to modify the clipboard content |
| Drag and drop | Users need to move content between areas visually | Copy/paste is the expected interaction pattern |
onCopy fires when the user copies (Ctrl+C / Cmd+C or context menu)onCut fires when the user cuts (Ctrl+X / Cmd+X or context menu)onPaste fires when the user pastes (Ctrl+V / Cmd+V or context menu)clipboardData property of type DataTransferWithout e.preventDefault(), the browser overwrites your custom clipboard data with the default copy behavior. You must call it before or after setData() to ensure your custom data is preserved.
Add tabIndex={0} (or tabIndex={-1} for programmatic-only focus) to the element so it becomes focusable. Clipboard events only fire on focused elements or their ancestors via bubbling.
const handlePaste = (e: React.ClipboardEvent) => {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
const file = items[i].getAsFile();
if (file) console.log(file.name, file.type);
}
}
};navigator.clipboard.writeText() is the modern async API, requires HTTPS and a user gesturedocument.execCommand("copy") is deprecated but works in insecure (HTTP) contextsnavigator.clipboard as primary, with execCommand as a fallbackNo. The DataTransfer object is cleared after the event handler completes. If you store the event and try to read clipboardData later (e.g., in a setTimeout or after await), the data will be gone. Extract all needed data synchronously within the handler.
const handleCopy = (e: React.ClipboardEvent) => {
e.preventDefault();
e.clipboardData.setData("text/plain", "plain version");
e.clipboardData.setData("text/html", "<b>HTML version</b>");
};writeText() only requires a recent user gesturereadText() triggers a browser permission prompt because reading clipboard is a privacy-sensitive operationonPaste event handlers to read pasted content instead of proactively reading the clipboardconst handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
const sanitized = text.replace(/[<>]/g, "");
document.execCommand("insertText", false, sanitized);
};When a user pastes a screenshot, the OS does not provide a meaningful file name. file.name is often empty or generic. Generate your own names using timestamps or context, such as pasted-${Date.now()}.png.
// Full event type
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.clipboardData; // DataTransfer
e.currentTarget; // HTMLInputElement
};
// Shorthand handler type
const onCut: React.ClipboardEventHandler<HTMLInputElement> = (e) => {};const handlePaste = (e: React.ClipboardEvent) => {
const items: DataTransferItemList = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item: DataTransferItem = items[i];
item.kind; // "string" | "file"
item.type; // MIME type string
if (item.kind === "file") {
const file: File | null = item.getAsFile();
}
}
};<input
type="email"
name="confirmEmail"
onPaste={(e) => e.preventDefault()}
/>Call e.preventDefault() in the onPaste handler to block the paste operation entirely.
Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥