JavaScript Clipboard API: A Complete Tutorial

A complete tutorial on the JavaScript Clipboard API. Covers navigator.clipboard.readText and writeText, read and write for rich content, ClipboardItem and MIME types, ClipboardEvent for copy cut and paste, building a copy-to-clipboard button, handling clipboard permissions, fallback with document.execCommand, and sanitizing pasted content.

JavaScriptintermediate
14 min read

The Clipboard API provides asynchronous methods to read from and write to the system clipboard. It replaces the deprecated document.execCommand('copy') approach with a Promise-based API that works with both plain text and rich content. This guide covers every method, event, and permission scenario.

Writing Text to Clipboard

javascriptjavascript
async function copyText(text) {
  try {
    await navigator.clipboard.writeText(text);
    console.log("Text copied to clipboard");
    return true;
  } catch (error) {
    console.error("Failed to copy:", error.message);
    return false;
  }
}
 
// Usage
await copyText("Hello, clipboard!");

Reading Text From Clipboard

javascriptjavascript
async function pasteText() {
  try {
    const text = await navigator.clipboard.readText();
    console.log("Clipboard contains:", text);
    return text;
  } catch (error) {
    console.error("Failed to read clipboard:", error.message);
    return null;
  }
}
 
// Requires clipboard-read permission (browser prompts user)
const content = await pasteText();

Clipboard Permissions

OperationPermissionUser Gesture RequiredBrowser Prompt
writeTextclipboard-writeYes (most browsers)Rarely
readTextclipboard-readYesAlways
write (rich)clipboard-writeYesRarely
read (rich)clipboard-readYesAlways

Checking Permission State

javascriptjavascript
async function checkClipboardPermission(type = "clipboard-read") {
  if (!navigator.permissions) return "unknown";
 
  try {
    const result = await navigator.permissions.query({ name: type });
    console.log(`${type} permission: ${result.state}`);
 
    result.addEventListener("change", () => {
      console.log(`${type} permission changed to: ${result.state}`);
    });
 
    return result.state; // "granted", "denied", or "prompt"
  } catch {
    return "unknown";
  }
}
 
const readPerm = await checkClipboardPermission("clipboard-read");
const writePerm = await checkClipboardPermission("clipboard-write");

Rich Content With ClipboardItem

Writing HTML and Images

javascriptjavascript
// Write HTML to clipboard
async function copyHTML(htmlString, plainText) {
  try {
    const htmlBlob = new Blob([htmlString], { type: "text/html" });
    const textBlob = new Blob([plainText], { type: "text/plain" });
 
    const item = new ClipboardItem({
      "text/html": htmlBlob,
      "text/plain": textBlob,
    });
 
    await navigator.clipboard.write([item]);
    console.log("Rich content copied");
    return true;
  } catch (error) {
    console.error("Failed to copy rich content:", error.message);
    return false;
  }
}
 
await copyHTML(
  "<strong>Bold text</strong> and <em>italic</em>",
  "Bold text and italic"
);
 
// Write an image to clipboard
async function copyImage(imageUrl) {
  try {
    const response = await fetch(imageUrl);
    const blob = await response.blob();
 
    const item = new ClipboardItem({
      [blob.type]: blob,
    });
 
    await navigator.clipboard.write([item]);
    console.log("Image copied to clipboard");
    return true;
  } catch (error) {
    console.error("Failed to copy image:", error.message);
    return false;
  }
}

Reading Rich Content

javascriptjavascript
async function readClipboard() {
  try {
    const items = await navigator.clipboard.read();
 
    for (const item of items) {
      console.log("Available types:", item.types);
 
      for (const type of item.types) {
        const blob = await item.getType(type);
 
        if (type.startsWith("text/")) {
          const text = await blob.text();
          console.log(`${type}:`, text);
        } else if (type.startsWith("image/")) {
          const url = URL.createObjectURL(blob);
          console.log(`Image URL:`, url);
          // Display: document.getElementById('preview').src = url;
        }
      }
    }
  } catch (error) {
    console.error("Failed to read clipboard:", error.message);
  }
}

ClipboardEvent: Copy, Cut, Paste

javascriptjavascript
// Intercept copy event
document.addEventListener("copy", (event) => {
  const selection = document.getSelection().toString();
 
  if (selection) {
    // Modify copied content
    event.clipboardData.setData("text/plain", selection.toUpperCase());
    event.clipboardData.setData(
      "text/html",
      `<strong>${selection.toUpperCase()}</strong>`
    );
    event.preventDefault(); // Prevent default copy behavior
  }
});
 
// Intercept cut event
document.addEventListener("cut", (event) => {
  const selection = document.getSelection().toString();
 
  if (selection) {
    event.clipboardData.setData("text/plain", selection);
    event.preventDefault();
 
    // Remove the selected content
    document.execCommand("delete");
  }
});
 
// Intercept paste event
document.addEventListener("paste", (event) => {
  const pastedText = event.clipboardData.getData("text/plain");
  const pastedHTML = event.clipboardData.getData("text/html");
 
  console.log("Pasted text:", pastedText);
  console.log("Pasted HTML:", pastedHTML);
 
  // Check for pasted files (images)
  const files = event.clipboardData.files;
  if (files.length > 0) {
    console.log("Pasted files:", files.length);
    for (const file of files) {
      console.log(`  ${file.name} (${file.type}, ${file.size} bytes)`);
    }
  }
});

Copy-to-Clipboard Button

javascriptjavascript
class CopyButton {
  constructor(button, options = {}) {
    this.button = button;
    this.originalText = button.textContent;
    this.successText = options.successText || "Copied!";
    this.errorText = options.errorText || "Failed";
    this.resetDelay = options.resetDelay || 2000;
    this.timer = null;
 
    this.button.addEventListener("click", () => this.copy());
  }
 
  async copy() {
    const target = this.button.dataset.copyTarget;
    const text = this.button.dataset.copyText;
 
    let content;
 
    if (text) {
      content = text;
    } else if (target) {
      const element = document.querySelector(target);
      content = element ? element.textContent : "";
    } else {
      content = "";
    }
 
    try {
      await navigator.clipboard.writeText(content);
      this.showFeedback(this.successText, "success");
    } catch {
      // Fallback for older browsers
      const success = this.fallbackCopy(content);
      this.showFeedback(
        success ? this.successText : this.errorText,
        success ? "success" : "error"
      );
    }
  }
 
  fallbackCopy(text) {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.position = "fixed";
    textarea.style.opacity = "0";
    document.body.appendChild(textarea);
    textarea.select();
 
    try {
      const success = document.execCommand("copy");
      return success;
    } catch {
      return false;
    } finally {
      document.body.removeChild(textarea);
    }
  }
 
  showFeedback(text, type) {
    clearTimeout(this.timer);
    this.button.textContent = text;
    this.button.dataset.copyState = type;
 
    this.timer = setTimeout(() => {
      this.button.textContent = this.originalText;
      delete this.button.dataset.copyState;
    }, this.resetDelay);
  }
}
 
// Usage in HTML:
// <button data-copy-target="#code-block">Copy</button>
// <pre id="code-block">const x = 42;</pre>
 
// Initialize all copy buttons
document.querySelectorAll("[data-copy-target], [data-copy-text]").forEach(
  (btn) => new CopyButton(btn)
);

Sanitizing Pasted Content

javascriptjavascript
function setupPasteSanitizer(element, options = {}) {
  const allowedTags = options.allowedTags || ["b", "i", "em", "strong", "a", "br"];
  const stripAll = options.plainTextOnly || false;
 
  element.addEventListener("paste", (event) => {
    event.preventDefault();
 
    let content;
 
    if (stripAll) {
      content = event.clipboardData.getData("text/plain");
      document.execCommand("insertText", false, content);
      return;
    }
 
    const html = event.clipboardData.getData("text/html");
    const text = event.clipboardData.getData("text/plain");
 
    if (html) {
      content = sanitizeHTML(html, allowedTags);
      document.execCommand("insertHTML", false, content);
    } else {
      document.execCommand("insertText", false, text);
    }
  });
}
 
function sanitizeHTML(html, allowedTags) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, "text/html");
  const body = doc.body;
 
  function clean(node) {
    const children = [...node.childNodes];
 
    for (const child of children) {
      if (child.nodeType === Node.TEXT_NODE) continue;
 
      if (child.nodeType === Node.ELEMENT_NODE) {
        const tag = child.tagName.toLowerCase();
 
        if (!allowedTags.includes(tag)) {
          // Replace disallowed tag with its children
          while (child.firstChild) {
            node.insertBefore(child.firstChild, child);
          }
          node.removeChild(child);
        } else {
          // Remove all attributes except href on links
          const attrs = [...child.attributes];
          for (const attr of attrs) {
            if (tag === "a" && attr.name === "href") continue;
            child.removeAttribute(attr.name);
          }
          clean(child);
        }
      } else {
        node.removeChild(child);
      }
    }
  }
 
  clean(body);
  return body.innerHTML;
}
 
// Usage
const editor = document.getElementById("rich-editor");
setupPasteSanitizer(editor, {
  allowedTags: ["b", "i", "em", "strong", "a", "br", "p"],
});
 
// Plain text only
const plainEditor = document.getElementById("plain-editor");
setupPasteSanitizer(plainEditor, { plainTextOnly: true });

Clipboard Utility Class

javascriptjavascript
class ClipboardUtil {
  static isSupported() {
    return "clipboard" in navigator;
  }
 
  static async writeText(text) {
    if (this.isSupported()) {
      await navigator.clipboard.writeText(text);
      return true;
    }
    return this.legacyCopy(text);
  }
 
  static async readText() {
    if (this.isSupported()) {
      return navigator.clipboard.readText();
    }
    return null;
  }
 
  static async writeRich(items) {
    const clipboardItems = items.map((item) => {
      const data = {};
      for (const [mimeType, content] of Object.entries(item)) {
        data[mimeType] = new Blob([content], { type: mimeType });
      }
      return new ClipboardItem(data);
    });
 
    await navigator.clipboard.write(clipboardItems);
  }
 
  static async readRich() {
    const items = await navigator.clipboard.read();
    const results = [];
 
    for (const item of items) {
      const entry = { types: item.types, data: {} };
      for (const type of item.types) {
        const blob = await item.getType(type);
        entry.data[type] = type.startsWith("text/")
          ? await blob.text()
          : blob;
      }
      results.push(entry);
    }
 
    return results;
  }
 
  static legacyCopy(text) {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.cssText = "position:fixed;opacity:0;left:-9999px";
    document.body.appendChild(textarea);
    textarea.select();
    const success = document.execCommand("copy");
    document.body.removeChild(textarea);
    return success;
  }
}
 
// Usage
await ClipboardUtil.writeText("Hello!");
const text = await ClipboardUtil.readText();
 
await ClipboardUtil.writeRich([
  {
    "text/html": "<b>Bold</b> text",
    "text/plain": "Bold text",
  },
]);

Supported MIME Types

MIME TypeBrowser SupportUse Case
text/plainAllPlain text copy/paste
text/htmlAllRich text with formatting
image/pngChrome, Edge, FirefoxScreenshot/image copy
image/svg+xmlLimitedSVG graphics
application/jsonThrough text/plainStructured data (encode as text)
Rune AI

Rune AI

Key Insights

  • Async and Promise-based: writeText and readText return Promises; always use await or .then() and handle rejections
  • User gesture required: Most clipboard operations need a click or key event trigger; background clipboard access is blocked by browsers
  • Provide legacy fallback: Use document.execCommand('copy') as a fallback for browsers that do not support the Clipboard API
  • Sanitize pasted content: Strip disallowed HTML tags and attributes from pasted rich content to prevent XSS in contenteditable elements
  • ClipboardItem for rich content: Use ClipboardItem with Blob objects and MIME types to copy HTML, images, and multiple formats simultaneously
RunePowered by Rune AI

Frequently Asked Questions

Does the Clipboard API work in all browsers?

`writeText` and `readText` are supported in all modern browsers (Chrome 66+, Firefox 63+, Safari 13.1+, Edge 79+). The `write`/`read` methods for rich content have more limited support. Always provide a fallback using `document.execCommand('copy')` for older browsers.

Why does clipboard.readText() require user permission?

Reading the clipboard is a privacy-sensitive operation. The clipboard may contain passwords, personal data, or sensitive content from other apps. Browsers always prompt for `clipboard-read` permission and require a user gesture (click, key press). Writing is less restricted because it replaces clipboard contents rather than exposing them.

Can I copy to clipboard without user interaction?

Most browsers require a user gesture (click, keyboard event) to trigger clipboard operations. Calling `writeText` outside a user gesture handler will be rejected with a `NotAllowedError`. Extensions and some privileged contexts may bypass this restriction.

How do I handle pasted images?

Listen for the `paste` event and check `event.clipboardData.files` for image files. Create an object URL with `URL.createObjectURL(file)` to display the image, or read it with `FileReader` for upload. The Clipboard API `read()` method also returns image blobs.

What is the maximum clipboard data size?

There is no standard limit, but browsers impose practical limits. Chrome limits text to ~2MB and images to ~100MB. Safari has stricter limits. Very large clipboard operations may fail silently or throw errors. For large data transfers, use IndexedDB or file download instead.

Conclusion

The Clipboard API provides a modern, Promise-based approach to clipboard operations. Use writeText/readText for plain text, write/read with ClipboardItem for rich content, and clipboard events for intercepting copy/cut/paste. Always provide a document.execCommand fallback for older browsers and sanitize pasted HTML to prevent XSS. For storing clipboard data persistently, see JS localStorage API guide. For handling the async nature of clipboard operations, see advanced JS promise patterns complete tutorial.