JavaScriptClipboard API

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

JS
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

JS
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.

JS
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

JS
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

JS
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

JS
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

JS
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.

JS
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 gesture
Browser support
Support is solid in Chrome, Edge, Safari and Firefox for text and most image types. The exact set of permitted MIME types in `write` varies; "image/png" and "text/html" are the safe bets.
A robust copy helper

copy.js

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;
}
execCommand is deprecated
The legacy `document.execCommand("copy")` still works in most current browsers but is officially deprecated. Use it only as a fallback, not as your primary path.