StackPractices
beginner Por Mathias Paulenko

Copiar y Pegar con Clipboard en JavaScript

Copia texto al portapapeles programáticamente en JavaScript con fallback

Temas: frontend

Visión General

La Clipboard API permite copiar y pegar texto programáticamente en el navegador. navigator.clipboard.writeText() es el enfoque moderno, con document.execCommand("copy") como fallback para navegadores antiguos. Esta recipe cubre copiar texto, leer del portapapeles, manejar permisos e interceptar eventos de pegado.

Cuándo Usar

  • For alternatives, see JavaScript Infinite Scroll Pagination with.

  • Necesitas un botón “Copiar al portapapeles” para snippets de código, URLs o tokens

  • Quieres leer contenido del portapapeles en eventos de pegado

  • Estás construyendo un editor de texto rico que intercepta el pegado

  • Necesitas soporte de portapapeles en navegadores modernos y legacy

Solución

Copiar al portapapeles básico

async function copyToClipboard(text) {
    try {
        await navigator.clipboard.writeText(text);
        console.log("Copied to clipboard");
        return true;
    } catch (err) {
        console.error("Failed to copy:", err);
        return false;
    }
}

// Uso
document.getElementById("copy-btn").addEventListener("click", () => {
    copyToClipboard("https://example.com/share-link");
});

Copiar con fallback para navegadores antiguos

async function copyToClipboard(text) {
    if (navigator.clipboard && window.isSecureContext) {
        try {
            await navigator.clipboard.writeText(text);
            return true;
        } catch (err) {
            // Caer al fallback
        }
    }

    // Fallback: execCommand
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.position = "fixed";
    textarea.style.left = "-9999px";
    document.body.appendChild(textarea);
    textarea.focus();
    textarea.select();

    try {
        const success = document.execCommand("copy");
        document.body.removeChild(textarea);
        return success;
    } catch (err) {
        document.body.removeChild(textarea);
        return false;
    }
}

// Uso
const button = document.querySelector("#copy-btn");
button.addEventListener("click", async () => {
    const ok = await copyToClipboard("text to copy");
    if (ok) {
        button.textContent = "Copied!";
        setTimeout(() => button.textContent = "Copy", 2000);
    }
});

Leer del portapapeles

async function readClipboard() {
    try {
        const text = await navigator.clipboard.readText();
        console.log("Clipboard content:", text);
        return text;
    } catch (err) {
        console.error("Failed to read clipboard:", err);
        return null;
    }
}

// Uso: leer portapapeles al hacer click (requiere gesto del usuario)
document.getElementById("paste-btn").addEventListener("click", async () => {
    const text = await readClipboard();
    if (text) {
        document.getElementById("output").value = text;
    }
});

Interceptar eventos de pegado

document.getElementById("editor").addEventListener("paste", (event) => {
    event.preventDefault();

    const clipboardData = event.clipboardData || window.clipboardData;
    const pastedText = clipboardData.getData("text/plain");

    // Sanitizar o transformar el contenido pegado
    const cleanText = pastedText
        .replace(/<[^>]*>/g, "")
        .replace(/\s+/g, " ")
        .trim();

    document.execCommand("insertText", false, cleanText);
});

// Manejar pegado de imágenes
document.getElementById("image-editor").addEventListener("paste", (event) => {
    const items = event.clipboardData?.items;
    if (!items) return;

    for (const item of items) {
        if (item.type.startsWith("image/")) {
            const file = item.getAsFile();
            const reader = new FileReader();

            reader.onload = (e) => {
                const img = document.createElement("img");
                img.src = e.target.result;
                document.getElementById("preview").appendChild(img);
            };

            reader.readAsDataURL(file);
        }
    }
});

Botón de copiar para bloques de código

function addCopyButtons() {
    const codeBlocks = document.querySelectorAll("pre code");

    codeBlocks.forEach((codeBlock) => {
        const button = document.createElement("button");
        button.className = "copy-btn";
        button.textContent = "Copy";
        button.style.position = "absolute";
        button.style.top = "8px";
        button.style.right = "8px";

        button.addEventListener("click", async () => {
            const text = codeBlock.textContent;
            const ok = await copyToClipboard(text);

            if (ok) {
                button.textContent = "Copied!";
                setTimeout(() => button.textContent = "Copy", 2000);
            }
        });

        const pre = codeBlock.parentElement;
        pre.style.position = "relative";
        pre.appendChild(button);
    });
}

addCopyButtons();

Verificar permisos del portapapeles

async function checkClipboardPermission() {
    try {
        const permission = await navigator.permissions.query({
            name: "clipboard-write"
        });

        if (permission.state === "granted") {
            return true;
        }

        if (permission.state === "prompt") {
            // Preguntará en la próxima operación del portapapeles
            return null;
        }

        return false;
    } catch (err) {
        // permissions API no soportada, asumir disponible
        return true;
    }
}

// Uso
const canCopy = await checkClipboardPermission();
if (canCopy === false) {
    showNotification("Clipboard access denied. Please enable it in browser settings.");
}

Explicación

La Clipboard API tiene dos métodos principales:

  • navigator.clipboard.writeText(text): Escribe texto al portapapeles. Retorna una promesa. Requiere un contexto seguro (HTTPS o localhost) y debe ser disparado por un gesto del usuario (click, keypress).
  • navigator.clipboard.readText(): Lee texto del portapapeles. Retorna una promesa. Requiere el permiso clipboard-read, que los navegadores suelen solicitar en el primer uso.

El fallback document.execCommand("copy") está deprecado pero sigue funcionando en navegadores antiguos. Requiere un elemento textarea visible u off-screen que esté seleccionado antes de llamar execCommand.

Restricciones clave:

  • Contexto seguro: navigator. clipboard solo funciona en HTTPS o localhost. En HTTP, usar el fallback.
  • Gesto del usuario: Las operaciones del portapapeles deben ser disparadas por una acción del usuario (click, tap, keypress). No se puede copiar programáticamente sin interacción del usuario.
  • Permisos: Leer el portapapeles requiere permiso explícito. Escribir suele estar permitido con un gesto del usuario.
  • Eventos de pegado: El evento paste se dispara en elementos focusable (inputs, textareas, contenteditable). clipboardDatapara acceder al contenido pegado yevent. preventDefault()` para sobreescribir el comportamiento por defecto.

Variantes

MétodoSoporteAsyncUsar Cuando
navigator.clipboard.writeTextNavegadores modernosOpción por defecto, HTTPS
document.execCommand("copy")Todos los navegadoresNoFallback para legacy
navigator.clipboard.readTextNavegadores modernosLeer portapapeles
Handler de evento pasteTodos los navegadoresNoInterceptar pegado

Pautas

  • Siempre usar navigator.clipboard primero, con execCommand como fallback.
  • Requerir HTTPS para la API moderna. No funciona en HTTP.
  • Dar feedback visual después de copiar (ej., texto “Copiado!” o cambio de icono).
  • Manejar errores gracefulmente. El acceso al portapapeles puede ser denegado por el usuario.
  • Sanitizar contenido pegado en editores de texto rico para prevenir XSS.
  • No leer el portapapeles sin interacción del usuario. Los navegadores lo bloquean.
  • Probar en navegadores móviles. Algunos tienen flujos de permisos diferentes.

Errores Comunes

  • Llamar navigator.clipboard en HTTP. Solo funciona en HTTPS o localhost.
  • No proveer un fallback para navegadores antiguos. La API moderna no es universal.
  • No manejar la promesa rechazada. El usuario puede denegar el permiso del portapapeles.
  • Olvidar remover el textarea off-screen en el fallback. Causa memory leaks.
  • Intentar leer el portapapeles sin un gesto del usuario. Los navegadores lo bloquean por seguridad.
  • No sanitizar HTML pegado. Pegar desde Word o navegadores puede inyectar markup malicioso.

Puntos Clave

  • Aplica copiar y pegar con clipboard en javascript cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Troubleshooting

  • Component does not re-render: verify state reference, props, and memoization. A mutated object can bypass change detection.
  • Style does not apply in production: check that CSS is loaded, class names are not mangled, and specificity wins. Purge unused styles carefully.
  • Build fails after dependency update: read the changelog, pin versions, and clean the lock file.
  • Accessibility audit fails: add labels, landmarks, focus management, and color contrast.
  • Hydration mismatch: ensure server and client render the same initial HTML. random, or window during SSR.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

¿Por qué navigator.clipboard no funciona en localhost con HTTP?

La Clipboard API requiere un contexto seguro. localhost es considerado seguro incluso sobre HTTP, pero otros orígenes HTTP no. Usa HTTPS en producción o prueba en localhost.

¿Cómo copio texto rico o HTML al portapapeles?

Usa navigator.clipboard.write() con un ClipboardItem:

const htmlBlob = new Blob(["<b>Bold text</b>"], { type: "text/html" });
const textBlob = new Blob(["Bold text"], { type: "text/plain" });

await navigator.clipboard.write([
    new ClipboardItem({
        "text/html": htmlBlob,
        "text/plain": textBlob
    })
]);
¿Puedo copiar sin un click de botón?

No. Los navegadores requieren un gesto del usuario (click, tap, keypress) para operaciones del portapapeles. No se puede copiar automáticamente al cargar la página o en un timeout sin interacción previa del usuario.

¿Cómo copio una imagen al portapapeles?

Usa navigator.clipboard.write() con un PNG blob:

const response = await fetch("image.png");
const blob = await response.blob();

await navigator.clipboard.write([
    new ClipboardItem({ "image/png": blob })
]);