Tips & Tricks (Actualizado: 2/6/2026)

Clipboard API con Claude Code: botones de copiar, permisos, fallback y tests

Implementa Clipboard API con Claude Code: copiar, pegar, permisos, fallback, React, privacidad y Playwright.

Clipboard API con Claude Code: botones de copiar, permisos, fallback y tests

La Clipboard API parece una mejora pequeña: un botón llama a navigator.clipboard.writeText() y el usuario copia texto. En producción no es tan simple. Puede fallar en una vista previa HTTP, dentro de un iframe, después de un fetch lento, con permisos diferentes entre navegadores o en un flujo de Safari móvil que exige una interacción reciente del usuario.

Esta guía muestra cómo pedirle a Claude Code una implementación lista para usar en React y TypeScript. Clipboard API es la API del navegador para leer y escribir en el portapapeles del sistema operativo. Async Clipboard es la versión basada en Promises. Secure context significa un entorno que el navegador considera confiable, como HTTPS o localhost. Fallback es el camino alternativo que probamos cuando la API moderna no está disponible.

Para ampliar el tema, revisa accesibilidad con Claude Code, tests Playwright con Claude Code y validación de formularios con Claude Code. Ten a mano las fuentes oficiales: MDN Clipboard API, MDN writeText, W3C Clipboard API and events, Playwright BrowserContext, WebKit Async Clipboard API y Claude Code docs.

Define criterios antes de implementar

No le pidas a Claude Code solo “haz un botón de copiar”. Dale los casos que deben funcionar y los límites que no debe cruzar.

Goal: Implement Clipboard API copy and paste UX in React.
Scope: edit only src/lib/clipboard.ts, src/components/CopyButton.tsx, and matching tests.
Requirements:
- Use navigator.clipboard.writeText in secure contexts.
- Keep the write call inside a user click handler.
- Provide a textarea fallback for unsupported or HTTP pages.
- Never read clipboard on page load.
- Show accessible copied/error feedback.
- Add Playwright tests for copy success and paste normalization.
Do not stage, commit, or edit unrelated files.

El flujo que buscamos es este.

flowchart TD
  A["El usuario pulsa copiar"] --> B{"Async Clipboard disponible?"}
  B -->|yes| C["writeText"]
  B -->|no| D["textarea + execCommand fallback"]
  C --> E{"Funcionó?"}
  D --> E
  E -->|yes| F["Anunciar copiado con aria-live"]
  E -->|no| G["Mostrar ayuda para copiar manualmente"]
  H["El usuario pega de forma explícita"] --> I["readText u onPaste"]
  I --> J["Normalizar, limitar, validar"]

La regla de privacidad es clara: no leas el portapapeles al cargar la página ni como acción oculta. Puede contener contraseñas, direcciones, enlaces internos, datos de clientes o código privado. La lectura debe ocurrir solo por un botón visible de pegar o por el evento normal onPaste.

Extrae la lógica de copia

Primero crea una utilidad independiente de React. Así los fallbacks, errores y tests quedan en un solo sitio.

// src/lib/clipboard.ts
export type CopyResult =
  | { ok: true; method: "async-clipboard" | "textarea-fallback" }
  | { ok: false; method: "async-clipboard" | "textarea-fallback" | "unsupported"; error: string };

export async function copyText(text: string): Promise<CopyResult> {
  if (!text) {
    return { ok: false, method: "unsupported", error: "Copy text is empty." };
  }

  if (canUseAsyncClipboard()) {
    try {
      await navigator.clipboard.writeText(text);
      return { ok: true, method: "async-clipboard" };
    } catch (error) {
      const fallback = fallbackCopyText(text);
      if (fallback) return { ok: true, method: "textarea-fallback" };

      return {
        ok: false,
        method: "async-clipboard",
        error: error instanceof Error ? error.message : "Clipboard write was blocked.",
      };
    }
  }

  if (fallbackCopyText(text)) {
    return { ok: true, method: "textarea-fallback" };
  }

  return {
    ok: false,
    method: "unsupported",
    error: "Clipboard API is unavailable in this browser or context.",
  };
}

function canUseAsyncClipboard(): boolean {
  return (
    typeof window !== "undefined" &&
    window.isSecureContext &&
    typeof navigator !== "undefined" &&
    Boolean(navigator.clipboard?.writeText)
  );
}

function fallbackCopyText(text: string): boolean {
  if (typeof document === "undefined") return false;

  const textarea = document.createElement("textarea");
  textarea.value = text;
  textarea.setAttribute("readonly", "");
  textarea.style.position = "fixed";
  textarea.style.top = "0";
  textarea.style.left = "-9999px";
  textarea.style.opacity = "0";

  const selection = document.getSelection();
  const selectedRange =
    selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;

  document.body.appendChild(textarea);
  textarea.focus();
  textarea.select();

  try {
    return document.execCommand("copy");
  } catch {
    return false;
  } finally {
    document.body.removeChild(textarea);

    if (selection && selectedRange) {
      selection.removeAllRanges();
      selection.addRange(selectedRange);
    }
  }
}

document.execCommand("copy") está obsoleto, así que no debe ser la ruta principal. Sigue siendo útil como último recurso en navegadores antiguos, webviews restringidas o páginas HTTP de prueba. También puede fallar si no se ejecuta dentro de una acción del usuario.

Hook React y botón accesible

El hook gestiona los estados de copiar, copiado y error. El componente anuncia el resultado con role="status" y aria-live.

// src/components/CopyButton.tsx
import { useCallback, useEffect, useRef, useState } from "react";
import { copyText, type CopyResult } from "../lib/clipboard";

type ClipboardStatus = "idle" | "copying" | "copied" | "failed";

export function useClipboard(resetAfter = 2000) {
  const [status, setStatus] = useState<ClipboardStatus>("idle");
  const [message, setMessage] = useState("");
  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (timerRef.current) window.clearTimeout(timerRef.current);
    };
  }, []);

  const copy = useCallback(
    async (text: string): Promise<CopyResult> => {
      if (timerRef.current) window.clearTimeout(timerRef.current);

      setStatus("copying");
      setMessage("Copying...");

      const result = await copyText(text);

      if (result.ok) {
        setStatus("copied");
        setMessage("Copied to clipboard.");
      } else {
        setStatus("failed");
        setMessage("Copy failed. Select the text and copy it manually.");
      }

      timerRef.current = window.setTimeout(() => {
        setStatus("idle");
        setMessage("");
      }, resetAfter);

      return result;
    },
    [resetAfter],
  );

  return { copy, status, message };
}

type CopyButtonProps = {
  text: string;
  label?: string;
  copiedLabel?: string;
  className?: string;
};

export function CopyButton({
  text,
  label = "Copy",
  copiedLabel = "Copied",
  className = "",
}: CopyButtonProps) {
  const { copy, status, message } = useClipboard();
  const isCopying = status === "copying";

  return (
    <div className="inline-flex items-center gap-2">
      <button
        type="button"
        className={className}
        onClick={() => void copy(text)}
        disabled={isCopying}
        aria-label={status === "copied" ? copiedLabel : label}
      >
        {status === "copied" ? copiedLabel : label}
      </button>
      <span role="status" aria-live="polite" className="sr-only">
        {message}
      </span>
    </div>
  );
}

El texto visible no basta. Quien usa lector de pantalla también necesita saber si la acción funcionó. Además, la región de estado es un punto de verificación estable para Playwright.

Copia de bloques de código sin saltos visuales

El caso más común es el bloque de código en documentación o artículos. En una primera versión de ClaudeCodeLab, Masa detectó que el botón pasaba de “Copy” a “Copied”, se hacía más ancho y movía el layout. Una anchura mínima evita ese detalle molesto.

// src/components/CodeBlockWithCopy.tsx
import { CopyButton } from "./CopyButton";

type CodeBlockWithCopyProps = {
  code: string;
  language?: string;
};

export function CodeBlockWithCopy({ code, language = "text" }: CodeBlockWithCopyProps) {
  return (
    <figure className="relative my-6 overflow-hidden rounded-md border border-slate-700 bg-slate-950">
      <figcaption className="flex min-h-10 items-center justify-between border-b border-slate-800 px-3 text-xs text-slate-300">
        <span>{language}</span>
        <CopyButton
          text={code}
          label="Copy code"
          copiedLabel="Copied"
          className="min-w-24 rounded bg-slate-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-400 disabled:opacity-60"
        />
      </figcaption>
      <pre tabIndex={0} className="overflow-x-auto p-4 text-sm leading-6">
        <code>{code}</code>
      </pre>
    </figure>
  );
}

El mismo botón puede copiar comandos CLI, enlaces de invitación, cupones, IDs de soporte o prompts generados. Pídele a Claude Code un componente reutilizable y luego ejemplos de uso, no una solución rígida para un solo bloque.

Pegar es entrada sensible

En formularios, prioriza el evento normal onPaste. Usa navigator.clipboard.readText() solo detrás de un botón explícito.

// src/components/PasteImportBox.tsx
import { useState } from "react";

export function normalizePastedText(input: string): string {
  return input
    .replace(/\r\n?/g, "\n")
    .replace(/\u0000/g, "")
    .slice(0, 10_000);
}

export function PasteImportBox() {
  const [value, setValue] = useState("");
  const [message, setMessage] = useState("");

  async function pasteFromClipboard() {
    if (!navigator.clipboard?.readText || !window.isSecureContext) {
      setMessage("Use your browser paste shortcut instead.");
      return;
    }

    try {
      const text = await navigator.clipboard.readText();
      setValue(normalizePastedText(text));
      setMessage("Pasted from clipboard.");
    } catch {
      setMessage("Paste was blocked. Use Ctrl+V or Cmd+V in the text area.");
    }
  }

  return (
    <section aria-labelledby="paste-import-title">
      <h2 id="paste-import-title">Import prompt</h2>
      <button type="button" onClick={pasteFromClipboard}>
        Paste from clipboard
      </button>
      <textarea
        value={value}
        onChange={(event) => setValue(event.currentTarget.value)}
        onPaste={(event) => {
          const text = event.clipboardData.getData("text/plain");
          if (!text) return;

          event.preventDefault();
          setValue(normalizePastedText(text));
          setMessage("Pasted text was normalized.");
        }}
        aria-describedby="paste-import-help"
      />
      <p id="paste-import-help" role="status" aria-live="polite">
        {message}
      </p>
    </section>
  );
}

No confíes en lo pegado. Limita longitud, normaliza saltos de línea, elimina caracteres de control y valida el formato esperado. Si aceptas HTML, pásalo por un sanitizador antes de renderizarlo y no lo envíes directamente a dangerouslySetInnerHTML.

Casos de uso y fallos comunes

CasoPunto claveFallo típico
Copiar código en docsCopiar solo el string del código y anunciar éxitoEl botón cambia de tamaño y mueve el layout
Copiar IDs de pedidosCopiar solo el ID, no toda la filaSe copian nombres o direcciones de clientes
Pegar logs en soporteNormalizar, limitar tamaño y buscar secretosTokens o cookies quedan guardados sin límite
Compartir invitacionesCopiar URL con vencimiento y mostrar la fechaEnlaces caducados generan tickets de soporte

También aplica a ClaudeCodeLab. Los comandos de un PDF gratuito, los pasos de un taller o una plantilla de diagnóstico para consultoría convierten mejor si se copian con precisión. Pero claves de licencia, correos de compradores y datos privados no deberían estar dentro de un único botón cómodo de copiar.

HTTP, iframes y Safari móvil

La Async Clipboard moderna requiere secure context. HTTPS y localhost suelen funcionar; una vista previa http://192.168.x.x puede no exponer navigator.clipboard. Prueba fallback y, si falla, muestra instrucciones de copia manual.

En iframes, los navegadores Chromium pueden requerir Permissions Policy o el atributo allow.

<iframe
  src="https://docs.example.com/embed"
  allow="clipboard-read; clipboard-write"
  title="Documentation preview"
></iframe>

En Safari y WebKit de iOS, mantén la llamada al portapapeles muy cerca del click o tap. Evita esperar un fetch, temporizadores, animaciones o cambios de ruta antes de writeText. Prepara el valor antes del click, copia dentro del handler y luego actualiza la UI. Para flujos críticos, prueba en dispositivos iOS reales, no solo en Chrome de escritorio.

Tests Playwright para el portapapeles

Concede permisos al mismo origin que visitas. Playwright advierte que el soporte de permisos cambia entre navegadores y versiones, así que no fuerces el mismo nivel de prueba en todos. Chromium puede validar el contenido real; WebKit puede validar el feedback visible.

// tests/clipboard.spec.ts
import { expect, test } from "@playwright/test";

const baseURL = "http://127.0.0.1:4173";

test.describe("clipboard UX", () => {
  test.beforeEach(async ({ context }) => {
    await context.grantPermissions(["clipboard-read", "clipboard-write"], {
      origin: baseURL,
    });
  });

  test("copies a code block", async ({ page }) => {
    await page.goto(`${baseURL}/docs/install`);

    await page.getByRole("button", { name: /copy code/i }).first().click();

    await expect(page.getByRole("status")).toContainText(/copied/i);
    await expect
      .poll(() => page.evaluate(() => navigator.clipboard.readText()))
      .toContain("npm");
  });

  test("normalizes pasted text", async ({ page }) => {
    await page.goto(`${baseURL}/support/import`);
    await page.evaluate(() => navigator.clipboard.writeText("line1\r\nline2\u0000"));

    await page.getByRole("button", { name: /paste from clipboard/i }).click();

    await expect(page.getByRole("textbox")).toHaveValue("line1\nline2");
  });
});

Si falla en CI, revisa primero el origin. http://localhost:4173 y http://127.0.0.1:4173 son distintos. Después revisa soporte de permisos en el proyecto de navegador y si las etiquetas traducidas rompen tus queries por rol.

Checklist de accesibilidad

  • Usa un button real, no un div clicable.
  • Anuncia éxito y error con role="status" y aria-live="polite".
  • Usa etiquetas específicas: “Copy code”, “Copy invite link”, “Copy order ID”.
  • Mantén visible el foco de teclado.
  • Define anchura mínima para evitar saltos de layout.
  • Si falla, explica la siguiente acción.
  • No comuniques éxito solo con color.
  • Lee el portapapeles solo tras una acción explícita de pegar.

Review enfocada con Claude Code

Después de implementar, no pidas una revisión genérica. Usa un prompt de riesgo concreto.

Review only clipboard-related changes.
Check:
1. Clipboard read is never triggered on page load.
2. writeText is called from a user action.
3. HTTP or unsupported browser fallback is handled.
4. copied/error feedback is accessible.
5. pasted text is normalized and size-limited.
6. Playwright tests grant permissions for the correct origin.
Return findings with file and line references.

En ClaudeCodeLab usamos funciones pequeñas como esta en entrenamientos porque obligan a cerrar el ciclo completo: leer especificación, implementar, diseñar fallback, probar navegadores, revisar accesibilidad y documentar. Para plantillas y guías, revisa productos ClaudeCodeLab. Para adopción en equipo y revisión de workflows, revisa Claude Code training.

Resultado al probarlo

En la UI de copia de código de Masa, el primer problema fue visual: el botón se ensanchaba después de copiar y movía el bloque. El segundo apareció en una vista previa móvil servida por HTTP, donde la ruta async no existía. La versión estable extrajo la copia a una utilidad, añadió fallback con textarea, mostró ayuda de copia manual, fijó el ancho del botón y configuró Playwright con permisos para el origin exacto. Clipboard API es una función pequeña, pero solo queda lista para producción cuando permisos, privacidad, accesibilidad y tests se diseñan juntos.

#Claude Code #Clipboard API #React #Playwright #Accesibilidad
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.