React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

clipboardcopycutpastedata-transferreact-19typescript

Clipboard Events

Intercept copy, cut, and paste operations for custom clipboard behavior.

Event Reference

EventFires WhenBubblesCancelableKey Property
onCopyUser copies selection (Ctrl+C / Cmd+C, or context menu)YesYesclipboardData: DataTransfer
onCutUser cuts selection (Ctrl+X / Cmd+X, or context menu)YesYesclipboardData: DataTransfer
onPasteUser pastes content (Ctrl+V / Cmd+V, or context menu)YesYesclipboardData: DataTransfer

All clipboard events provide a clipboardData property of type DataTransfer that 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.writeText for 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 clipboardData property is a DataTransfer object that provides getData(format), setData(format, data), and items for file access.
  • Calling e.preventDefault() is required when using setData -- 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 have tabIndex to 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 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.

Alternatives

AlternativeUse WhenDon't Use When
navigator.clipboard APIYou need programmatic read/write without a clipboard eventYou are intercepting user-initiated copy/paste
document.execCommand("copy")You need clipboard access in insecure (HTTP) contextsYou can use the modern Clipboard API
Third-party libraries (e.g., clipboard.js)You need cross-browser clipboard support with minimal codeThe native API and React events cover your needs
onKeyDown Ctrl+C detectionYou need to know when the user attempts to copy without intercepting itYou need to modify the clipboard content
Drag and dropUsers need to move content between areas visuallyCopy/paste is the expected interaction pattern

FAQs

What are the three clipboard events React provides, and when does each one fire?
  • 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)
  • All three provide a clipboardData property of type DataTransfer
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 gesture
  • document.execCommand("copy") is deprecated but works in insecure (HTTP) contexts
  • Use navigator.clipboard as primary, with execCommand as 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 gesture
  • readText() triggers a browser permission prompt because reading clipboard is a privacy-sensitive operation
  • Prefer using onPaste event 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.