Clipboard API mit Claude Code: Copy-Buttons, Berechtigungen, Fallbacks und Tests
Clipboard API mit Claude Code umsetzen: Kopieren, Einfügen, Berechtigungen, Fallback, React und Playwright.
Die Clipboard API wirkt wie eine kleine Aufgabe: Ein Button ruft navigator.clipboard.writeText() auf, und der Text liegt in der Zwischenablage. In echten Produkten scheitert genau diese Stelle aber oft. HTTP-Preview statt HTTPS, iframe, verzögerter fetch, abweichende Browserberechtigungen oder Mobile Safari mit strenger Nutzeraktivierung reichen aus, damit ein scheinbar einfacher Copy-Button unzuverlässig wird.
Dieser Leitfaden zeigt, wie du Claude Code nutzt, um eine produktionsnahe Copy-and-Paste-UX in React und TypeScript zu bauen. Clipboard API meint die Browser-API zum Lesen und Schreiben der Zwischenablage des Betriebssystems. Async Clipboard ist die Promise-basierte Variante. Secure context bedeutet eine vom Browser als vertrauenswürdig eingestufte Umgebung, zum Beispiel HTTPS oder localhost. Fallback ist der Ersatzweg, wenn die moderne API nicht verfügbar ist.
Passende ClaudeCodeLab-Artikel sind Barrierefreiheit mit Claude Code, Playwright-Tests mit Claude Code und Formularvalidierung mit Claude Code. Die offiziellen Quellen sind MDN Clipboard API, MDN writeText, W3C Clipboard API and events, Playwright BrowserContext, WebKit Async Clipboard API und Claude Code docs.
Erst Akzeptanzkriterien festlegen
Bitte Claude Code nicht nur um einen Copy-Button. Übergib die relevanten Fehlermodi direkt.
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.
Der Zielablauf sieht so aus.
flowchart TD
A["Nutzer klickt Kopieren"] --> B{"Async Clipboard verfügbar?"}
B -->|yes| C["writeText"]
B -->|no| D["textarea + execCommand fallback"]
C --> E{"Erfolgreich?"}
D --> E
E -->|yes| F["Kopiert mit aria-live melden"]
E -->|no| G["Manuelles Kopieren erklären"]
H["Nutzer fügt bewusst ein"] --> I["readText oder onPaste"]
I --> J["Normalisieren, begrenzen, validieren"]
Die wichtigste Datenschutzregel: Lies die Zwischenablage nie beim Laden der Seite und nie als versteckte Hintergrundaktion. Dort können Passwörter, Adressen, interne URLs, Kundendaten oder Quellcode stehen. Lesen sollte nur über eine sichtbare Einfügeaktion oder das normale onPaste-Event erfolgen.
Kopierlogik als Utility bauen
Zuerst kommt eine React-unabhängige Funktion. So bleiben Fallbacks, Fehler und Tests an einer Stelle.
// 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") ist veraltet und sollte nicht der Hauptpfad sein. Als letzter Ausweg für ältere Browser, eingeschränkte WebViews oder HTTP-Previews ist es trotzdem nützlich. Auch dieser Weg kann fehlschlagen, wenn er nicht durch eine Nutzeraktion ausgelöst wird.
React-Hook und barrierefreier Button
Der Hook verwaltet Kopierstatus, Erfolg und Fehler. Der Button meldet das Ergebnis über role="status" und 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>
);
}
Ein sichtbarer Toast reicht nicht. Auch Nutzer mit Screenreader brauchen eine Rückmeldung. Die Statusregion ist außerdem ein stabiler Prüfpunkt für Playwright.
Codeblöcke ohne Layout Shift kopieren
Der häufigste Anwendungsfall ist der Codeblock in Dokumentation oder Blogartikeln. In einer frühen ClaudeCodeLab-Version sah Masa, dass der Button nach dem Wechsel von “Copy” zu “Copied” breiter wurde und den Codeblock verschob. Eine Mindestbreite verhindert das.
// 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>
);
}
Der gleiche Button kann CLI-Befehle, Einladungslinks, Gutscheincodes, Support-IDs oder generierte Prompts kopieren. Bitte Claude Code um eine wiederverwendbare Komponente und danach um Einsatzbeispiele.
Einfügen wie sensible Eingabe behandeln
In Formularen ist das normale onPaste-Event meist der bessere Weg. navigator.clipboard.readText() gehört hinter einen klar sichtbaren Einfügebutton.
// 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>
);
}
Eingefügter Inhalt ist nicht vertrauenswürdig. Begrenze die Länge, normalisiere Zeilenumbrüche, entferne Steuerzeichen und validiere das erwartete Format. Wenn HTML erlaubt ist, muss es vor dem Rendern bereinigt werden und darf nicht direkt in dangerouslySetInnerHTML landen.
Praxisfälle und typische Fehler
| Anwendungsfall | Umsetzung | Typischer Fehler |
|---|---|---|
| Code in Docs kopieren | Nur den Code-String kopieren und Erfolg melden | Buttontext verschiebt das Layout |
| Bestell-ID im Admin kopieren | Nur die ID, nicht die ganze Tabellenzeile | Kundennamen oder Adressen werden mitkopiert |
| Logs in Support-Tool einfügen | Normalisieren, begrenzen, Secrets prüfen | Tokens oder Cookies werden unbegrenzt gespeichert |
| Einladungslink teilen | Zeitlich begrenzte URL kopieren und Ablauf anzeigen | Abgelaufene Links erzeugen Supportaufwand |
Das gilt auch für ClaudeCodeLab-Produkte. Befehle aus einem kostenlosen PDF, Workshop-Setup-Schritte oder Diagnose-Prompts für Beratung müssen exakt kopierbar sein. Lizenzschlüssel, Käufer-E-Mails und Kundendaten gehören dagegen nicht in eine bequeme Sammelkopie.
HTTP, iframes und Mobile Safari
Async Clipboard setzt in modernen Browsern einen secure context voraus. HTTPS und localhost funktionieren meist; http://192.168.x.x kann navigator.clipboard gar nicht anbieten. Versuche den Fallback und zeige danach manuelles Kopieren an.
In iframes benötigen Chromium-Browser eventuell Permissions Policy oder das allow-Attribut.
<iframe
src="https://docs.example.com/embed"
allow="clipboard-read; clipboard-write"
title="Documentation preview"
></iframe>
Bei Safari und iOS WebKit muss der Clipboard-Aufruf eng am Klick oder Tap bleiben. Warte vor writeText nicht auf fetch, Timer, Animationen oder Navigation. Bereite den Wert vorher vor, kopiere sofort im Handler und aktualisiere danach die Oberfläche. Kritische Flows sollten auf echten iOS-Geräten geprüft werden.
Playwright-Tests für Clipboard UX
Erteile Berechtigungen für genau den Origin, den du besuchst. Playwright weist darauf hin, dass Berechtigungen je nach Browser und Version unterschiedlich unterstützt werden. In der Praxis kann Chromium den echten Clipboard-Inhalt prüfen, während WebKit sichtbares Feedback prüft.
// 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");
});
});
Wenn CI fehlschlägt, prüfe zuerst den Origin. http://localhost:4173 und http://127.0.0.1:4173 sind verschieden. Prüfe danach die Browserberechtigungen und ob übersetzte Buttonlabels deine Role-Queries ändern.
Barrierefreiheits-Checkliste
- Einen echten
buttonverwenden, keinen klickbarendiv. - Erfolg und Fehler mit
role="status"undaria-live="polite"melden. - Labels konkret formulieren: “Copy code”, “Copy invite link”, “Copy order ID”.
- Tastaturfokus sichtbar halten.
- Mindestbreite setzen, damit kein Layout Shift entsteht.
- Bei Fehlern den nächsten Schritt erklären.
- Erfolg nicht nur über Farbe kommunizieren.
- Die Zwischenablage nur nach einer expliziten Einfügeaktion lesen.
Claude Code gezielt prüfen lassen
Nach der Umsetzung hilft ein enger Review-Prompt mehr als eine allgemeine Bitte.
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.
ClaudeCodeLab nutzt solche kleinen Web-API-Funktionen gern in Trainings, weil sie den kompletten Engineering-Loop erzwingen: Spezifikation lesen, implementieren, Fallback entwerfen, Browser testen, Barrierefreiheit prüfen und dokumentieren. Vorlagen und Praxisguides findest du bei den ClaudeCodeLab-Produkten. Für Teameinführung und Review-Prozesse gibt es Claude Code Training.
Ergebnis aus dem Praxistest
Bei Masas Codeblock-Copy-UI war das erste Problem visuell: Nach dem Kopieren wurde der Button breiter und verschob den Block. Das zweite Problem trat auf einer mobilen HTTP-Preview auf, in der Async Clipboard nicht verfügbar war. Die stabile Version extrahierte die Logik in ein Utility, ergänzte textarea-Fallback und manuelle Anleitung, fixierte die Buttonbreite und ließ Playwright Berechtigungen für den exakten Origin setzen. Clipboard API ist klein, wird aber erst produktionsreif, wenn Berechtigungen, Datenschutz, Barrierefreiheit und Tests zusammen entworfen werden.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Permission Safety Ladder: Zugriff kontrolliert erweitern
Von read-only zu begrenzten Änderungen, Prüfbefehlen und Deploy-Checks mit klarer Kontrolle.
Claude Code Small PR Proof Pack: kleine Änderungen reviewbar machen
Ein Proof Pack für Claude-Code-PRs: Diff, Checks, öffentliche URL, CTA-Pfad und Rollback.
Claude-Code-Review-Gate vor dem Commit
Vor dem Commit mit Claude Code prüfen: Diff, Build, öffentliche URL, Gumroad-Links, Beratung-CTA, fehlende Tests und fremde Dateien.