Eventos del Portapapeles
Intercepta operaciones de copiar, cortar y pegar para comportamiento personalizado del portapapeles.
Busca en todas las páginas de la documentación
Intercepta operaciones de copiar, cortar y pegar para comportamiento personalizado del portapapeles.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Evento | Se Dispara Cuando | Burbujas | Cancelable | Propiedad Clave |
|---|---|---|---|---|
onCopy | El usuario copia una selección (Ctrl+C / Cmd+C, o menú contextual) | Sí | Sí | clipboardData: DataTransfer |
onCut | El usuario corta una selección (Ctrl+X / Cmd+X, o menú contextual) | Sí | Sí | clipboardData: DataTransfer |
onPaste | El usuario pega contenido (Ctrl+V / Cmd+V, o menú contextual) | Sí | Sí | clipboardData: DataTransfer |
Todos los eventos del portapapeles proporcionan una propiedad
clipboardDatade tipoDataTransferque te permite leer o escribir datos en múltiples formatos (text/plain, text/html, tipos MIME personalizados).
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Añade atribución al copiar texto
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(); // Requerido para usar datos personalizados del portapapeles
};
return <div onCopy={handleCopy}>{children}</div>;
}
// Desinfecta entrada pegada
function SanitizedInput() {
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
const sanitized = text.replace(/[<>]/g, ""); // elimina corchetes angulares
document.execCommand("insertText", false, sanitized);
};
return <input onPaste={handlePaste} placeholder="Paste here (sanitized)" />;
}Cuándo usarlo: Necesitas añadir atribución al texto copiado, desinfectar entrada pegada, manejar pegado de imágenes para cargas, o implementar un botón de copia personalizado.
// 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 {
// Alternativa para contextos inseguros
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>
);
}Lo que esto demuestra:
navigator.clipboard.writeText para copia asíncrona moderna con alternativa para contextos insegurosclipboardData.itemstabIndex={0} para que pueda recibir foco y eventos del portapapelesClipboardEvent nativo. React los normaliza entre navegadores.clipboardData es un objeto DataTransfer que proporciona getData(format), setData(format, data), e items para acceso a archivos.e.preventDefault() es requerido cuando se usa setData -- sin esto, el navegador sobrescribe tus datos personalizados con el comportamiento de copia predeterminado.<div> debe tener tabIndex para ser enfocable y recibir eventos de pegado.Copiar texto plano al portapapeles (programático, sin evento):
function CopyButton({ text }: { text: string }) {
const handleClick = async () => {
await navigator.clipboard.writeText(text);
};
return <button onClick={handleClick}>Copy</button>;
}Manejo de pegado de imágenes con verificación de tipo:
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;
// Usa dataUrl para vista previa o carga
};
reader.readAsDataURL(file);
};
return <div onPaste={handlePaste} tabIndex={0}>Paste an image here</div>;
}Formato de copia personalizado (HTML + texto plano):
function RichCopyTable({ rows }: { rows: string[][] }) {
const handleCopy = (e: React.ClipboardEvent) => {
e.preventDefault();
// Establece versiones de texto plano y HTML
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>
);
}Prevenir pegado en campos de confirmación:
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();
// Opcionalmente muestra un tooltip: "Please type your email again"
}}
/>
</div>
);
}Usar la API Clipboard (navigator.clipboard) para lectura:
function ClipboardReader() {
const [content, setContent] = useState("");
const readClipboard = async () => {
try {
// Requiere gesto del usuario y permisos
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>
);
}// El genérico ClipboardEvent especifica el elemento
const handleCopy = (e: React.ClipboardEvent<HTMLDivElement>) => {
e.clipboardData; // DataTransfer (siempre disponible en eventos del portapapeles)
e.currentTarget; // HTMLDivElement
};
// Los métodos de clipboardData están completamente tipados
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
const text: string = e.clipboardData.getData("text/plain");
const html: string = e.clipboardData.getData("text/html");
// items es una DataTransferItemList
const items: DataTransferItemList = e.clipboardData.items;
// Iterando items
for (let i = 0; i < items.length; i++) {
const item: DataTransferItem = items[i];
item.kind; // "string" | "file"
item.type; // cadena de tipo MIME
if (item.kind === "file") {
const file: File | null = item.getAsFile();
}
}
};
// Usando el tipo de manejador abreviado
const onCut: React.ClipboardEventHandler<HTMLTextAreaElement> = (e) => {
// e es React.ClipboardEvent<HTMLTextAreaElement>
};
// Los tipos de la API navigator.clipboard (incorporados en 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() es requerido cuando se usa setData -- Si llamas a e.clipboardData.setData() sin e.preventDefault(), el navegador reemplaza tus datos personalizados con la copia predeterminada. Solución: Siempre llama a e.preventDefault() antes o después de setData.
clipboardData solo está disponible durante el evento -- El objeto DataTransfer se borra después de que el manejador del evento se completa. No puedes almacenar el evento y leer clipboardData de forma asíncrona. Solución: Extrae los datos que necesitas de forma síncrona dentro del manejador y almacénalos en una variable o estado.
navigator.clipboard requiere HTTPS y activación del usuario -- La API Clipboard asíncrona solo funciona en contextos seguros (HTTPS o localhost) y requiere un gesto del usuario reciente (clic, pulsación de tecla). Solución: Proporciona una alternativa usando document.execCommand("copy") para contextos HTTP, y siempre llama a métodos del portapapeles desde manejadores de eventos.
navigator.clipboard.readText() dispara un aviso de permiso del navegador -- A diferencia de escribir, leer el portapapeles requiere permiso explícito del usuario. Este aviso puede confundir a los usuarios. Solución: Usa manejadores de eventos onPaste para leer contenido pegado en lugar de leer proactivamente el portapapeles, a menos que tu UX lo requiera específicamente.
Los archivos pegados tienen nombres vacíos o genéricos -- Cuando un usuario pega una captura de pantalla, file.name a menudo está vacío o algo como image.png. Solución: Genera nombres significativos usando marcas de tiempo o contexto, como pasted-${Date.now()}.png.
document.execCommand("copy") está deprecado -- Aunque aún es ampliamente compatible, puede eliminarse en navegadores futuros. Solución: Usa navigator.clipboard.writeText() como método principal con execCommand como alternativa solo.
Los elementos no-input no pueden recibir eventos de pegado sin foco -- Un <div> no disparará onPaste a menos que esté enfocado. Solución: Añade tabIndex={0} o tabIndex={-1} para hacer el contenedor enfocable, e instruye a los usuarios a hacer clic/enfocar el área primero.
| Alternativa | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
navigator.clipboard API | Necesitas lectura/escritura programática sin evento del portapapeles | Estás interceptando copiar/pegar iniciado por el usuario |
document.execCommand("copy") | Necesitas acceso al portapapeles en contextos inseguros (HTTP) | Puedes usar la API Clipboard moderna |
Bibliotecas de terceros (p. ej., clipboard.js) | Necesitas compatibilidad entre navegadores con código mínimo | La API nativa y los eventos React cubren tus necesidades |
onKeyDown detección Ctrl+C | Necesitas saber cuándo el usuario intenta copiar sin interceptarlo | Necesitas modificar el contenido del portapapeles |
| Arrastrar y soltar | Los usuarios necesitan mover contenido entre áreas visualmente | Copiar/pegar es el patrón de interacción esperado |
onCopy se dispara cuando el usuario copia (Ctrl+C / Cmd+C o menú contextual)onCut se dispara cuando el usuario corta (Ctrl+X / Cmd+X o menú contextual)onPaste se dispara cuando el usuario pega (Ctrl+V / Cmd+V o menú contextual)clipboardData de tipo DataTransferSin e.preventDefault(), el navegador sobrescribe tus datos personalizados del portapapeles con el comportamiento de copia predeterminado. Debes llamarlo antes o después de setData() para asegurar que tus datos personalizados se preservan.
Añade tabIndex={0} (o tabIndex={-1} para foco solo programático) al elemento para que sea enfocable. Los eventos del portapapeles solo se disparan en elementos enfocados o sus ancestros mediante burbujeo.
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() es la API asíncrona moderna, requiere HTTPS y un gesto del usuariodocument.execCommand("copy") está deprecado pero funciona en contextos inseguros (HTTP)navigator.clipboard como principal, con execCommand como alternativaNo. El objeto DataTransfer se borra después de que el manejador del evento se completa. Si almacenas el evento e intentas leer clipboardData más tarde (p. ej., en un setTimeout o después de await), los datos habrán desaparecido. Extrae todos los datos necesarios de forma síncrona dentro del manejador.
const handleCopy = (e: React.ClipboardEvent) => {
e.preventDefault();
e.clipboardData.setData("text/plain", "plain version");
e.clipboardData.setData("text/html", "<b>HTML version</b>");
};writeText() solo requiere un gesto del usuario recientereadText() dispara un aviso de permiso del navegador porque leer el portapapeles es una operación sensible a privacidadonPaste para leer contenido pegado en lugar de leer proactivamente el portapapelesconst handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
const sanitized = text.replace(/[<>]/g, "");
document.execCommand("insertText", false, sanitized);
};Cuando un usuario pega una captura de pantalla, el SO no proporciona un nombre de archivo significativo. file.name a menudo está vacío o es genérico. Genera tus propios nombres usando marcas de tiempo o contexto, como pasted-${Date.now()}.png.
// Tipo de evento completo
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.clipboardData; // DataTransfer
e.currentTarget; // HTMLInputElement
};
// Tipo de manejador abreviado
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; // cadena de tipo MIME
if (item.kind === "file") {
const file: File | null = item.getAsFile();
}
}
};<input
type="email"
name="confirmEmail"
onPaste={(e) => e.preventDefault()}
/>Llama a e.preventDefault() en el manejador onPaste para bloquear completamente la operación de pegado.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥