Clipboard API
Reading from and writing to the system clipboard used to be a maze of document.execCommand("copy"), temporary text areas, and per-browser quirks. The modern Async Clipboard API replaces that with a small promise-based surface on navigator.clipboard. It is secure-by-default, asynchronous, and supports text and rich content. This page covers the everyday calls and the permission and security rules you need to know.
The shape of the API
navigator.clipboard.writeText(text); // → Promise<void> navigator.clipboard.readText(); // → Promise<string> navigator.clipboard.write(items); // → Promise<void> (rich content) navigator.clipboard.read(); // → Promise<ClipboardItem[]>
All four return promises. They only resolve in a secure context (https: or localhost) and most require the page to be focused and the call to happen during a user gesture.
Copying text
copy a value to the clipboard
async function copy(text) {
try {
await navigator.clipboard.writeText(text);
flash("Copied!");
} catch (err) {
flash("Copy failed — please copy manually");
}
}
button.addEventListener("click", () => copy(code.textContent));Tying the call to a click satisfies the user-gesture rule on every browser. Calling it from a timer or an unsolicited script will reject in most environments.
Reading text
Reading from the clipboard is more sensitive than writing — the content could be a password or other personal data. Browsers prompt the user the first time, and only allow it from a user gesture.
pasteButton.addEventListener("click", async () => {
try {
const text = await navigator.clipboard.readText();
input.value = text;
} catch (err) {
// user denied, or page not focused
}
});Rich content with ClipboardItem
To copy images or HTML, build a ClipboardItem mapping MIME types to Blob (or Promise<Blob>) values.
copy an image blob
async function copyImage(canvas) {
const blob = await new Promise((res) => canvas.toBlob(res, "image/png"));
const item = new ClipboardItem({ "image/png": blob });
await navigator.clipboard.write([item]);
}copy as both HTML and plain text
const item = new ClipboardItem({
"text/html": new Blob(["<b>Hello</b>"], { type: "text/html" }),
"text/plain": new Blob(["Hello"], { type: "text/plain" }),
});
await navigator.clipboard.write([item]);Reading rich content
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
const blob = await item.getType(type);
console.log(type, blob.size, "bytes");
}
}Clipboard events
The classic copy, cut and paste events still fire on elements and are how you customise the content the user sees during a system copy.
override what gets copied from a selection
document.addEventListener("copy", (e) => {
e.preventDefault();
e.clipboardData.setData("text/plain", "Custom replacement text");
});
document.addEventListener("paste", (e) => {
const pasted = e.clipboardData.getData("text/plain");
console.log("pasted:", pasted);
});These events use the synchronous event.clipboardData object — useful when you want to intercept and transform a paste rather than initiate one.
Permissions API integration
You can query and observe the clipboard permission state ahead of time.
const read = await navigator.permissions.query({ name: "clipboard-read" });
const write = await navigator.permissions.query({ name: "clipboard-write" });
read.state; // "granted" | "denied" | "prompt"
write.state; // usually "granted" implicitly during a user gestureA robust copy helper
copy.js
export async function copyToClipboard(text) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// fall through to the legacy path
}
}
// legacy fallback for ancient browsers and insecure contexts
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.append(ta);
ta.select();
const ok = document.execCommand("copy");
ta.remove();
return ok;
}