Clipboard Events
Intercept copy, cut, and paste operations for custom clipboard behavior.
Event Reference
| 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).
Recipe
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.
Working Example
// 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:
- Using
navigator.clipboard.writeTextfor modern async copy with a fallback for insecure contexts - Intercepting paste events to extract files from
clipboardData.items - Creating object URLs for image previews of pasted files
- Making a non-input element pasteable with
tabIndex={0}so it can receive focus and clipboard events
Deep Dive
How It Works
- Clipboard events are synthetic events that wrap the native
ClipboardEvent. React normalizes them across browsers. - The
clipboardDataproperty is aDataTransferobject that providesgetData(format),setData(format, data), anditemsfor file access. - Calling
e.preventDefault()is required when usingsetData-- without it, the browser overwrites your custom data with the default copy behavior. - Clipboard events only fire on focused elements or their ancestors (due to bubbling). A
<div>must havetabIndexto be focusable and receive paste events.
Variations
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>
);
}TypeScript Notes
// 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();
}Gotchas
-
e.preventDefault()is required when usingsetData-- If you calle.clipboardData.setData()withoute.preventDefault(), the browser replaces your custom data with the default copy. Fix: Always calle.preventDefault()before or aftersetData. -
clipboardDatais only available during the event -- TheDataTransferobject is cleared after the event handler completes. You cannot store the event and readclipboardDataasynchronously. Fix: Extract the data you need synchronously within the handler and store it in a variable or state. -
navigator.clipboardrequires 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 usingdocument.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: UseonPasteevent 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.nameis often empty or something likeimage.png. Fix: Generate meaningful names using timestamps or context, likepasted-${Date.now()}.png. -
document.execCommand("copy")is deprecated -- While still widely supported, it may be removed in future browsers. Fix: Usenavigator.clipboard.writeText()as the primary method withexecCommandas a fallback only. -
Non-input elements cannot receive paste events without focus -- A
<div>will not fireonPasteunless it is focused. Fix: AddtabIndex={0}ortabIndex={-1}to make the container focusable, and instruct users to click/focus the area first.
Alternatives
| 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 |
FAQs
What are the three clipboard events React provides, and when does each one fire?
onCopyfires when the user copies (Ctrl+C / Cmd+C or context menu)onCutfires when the user cuts (Ctrl+X / Cmd+X or context menu)onPastefires when the user pastes (Ctrl+V / Cmd+V or context menu)- All three provide a
clipboardDataproperty of typeDataTransfer
Why is calling e.preventDefault() required when using e.clipboardData.setData()?
Without 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.
How do you make a non-input element like a div receive paste events?
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.
How do you read pasted files (like images) from clipboard data?
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);
}
}
};What is the difference between navigator.clipboard.writeText() and document.execCommand("copy")?
navigator.clipboard.writeText()is the modern async API, requires HTTPS and a user gesturedocument.execCommand("copy")is deprecated but works in insecure (HTTP) contexts- Use
navigator.clipboardas primary, withexecCommandas a fallback
Gotcha: Can you read clipboardData asynchronously after the event handler returns?
No. 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.
How do you set both plain text and HTML formats when intercepting a copy event?
const handleCopy = (e: React.ClipboardEvent) => {
e.preventDefault();
e.clipboardData.setData("text/plain", "plain version");
e.clipboardData.setData("text/html", "<b>HTML version</b>");
};Why does navigator.clipboard.readText() behave differently from writeText()?
writeText()only requires a recent user gesturereadText()triggers a browser permission prompt because reading clipboard is a privacy-sensitive operation- Prefer using
onPasteevent handlers to read pasted content instead of proactively reading the clipboard
How do you sanitize pasted input to strip unwanted characters?
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
const sanitized = text.replace(/[<>]/g, "");
document.execCommand("insertText", false, sanitized);
};Gotcha: Why might pasted files have empty or generic names like "image.png"?
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.
What is the correct TypeScript type for a clipboard event handler on an input element?
// Full event type
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.clipboardData; // DataTransfer
e.currentTarget; // HTMLInputElement
};
// Shorthand handler type
const onCut: React.ClipboardEventHandler<HTMLInputElement> = (e) => {};How do you type the items in clipboardData when iterating over pasted content in TypeScript?
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();
}
}
};How do you prevent users from pasting into a confirmation field?
<input
type="email"
name="confirmEmail"
onPaste={(e) => e.preventDefault()}
/>Call e.preventDefault() in the onPaste handler to block the paste operation entirely.
Related
- Drag & Drop Events -- Native HTML5 drag and drop with DataTransfer
- Form Events -- Handling input changes and form submission
- Keyboard Events -- Detecting key combinations like Ctrl+C