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.
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
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
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
| Operation | Permission | User Gesture Required | Browser Prompt |
|---|---|---|---|
writeText | clipboard-write | Yes (most browsers) | Rarely |
readText | clipboard-read | Yes | Always |
write (rich) | clipboard-write | Yes | Rarely |
read (rich) | clipboard-read | Yes | Always |
Checking Permission State
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
// 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
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
// 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
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
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
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 Type | Browser Support | Use Case |
|---|---|---|
text/plain | All | Plain text copy/paste |
text/html | All | Rich text with formatting |
image/png | Chrome, Edge, Firefox | Screenshot/image copy |
image/svg+xml | Limited | SVG graphics |
application/json | Through text/plain | Structured data (encode as text) |
Rune AI
Key Insights
- Async and Promise-based:
writeTextandreadTextreturn Promises; always useawaitor.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
ClipboardItemwithBlobobjects and MIME types to copy HTML, images, and multiple formats simultaneously
Frequently Asked Questions
Does the Clipboard API work in all browsers?
Why does clipboard.readText() require user permission?
Can I copy to clipboard without user interaction?
How do I handle pasted images?
What is the maximum clipboard data size?
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.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.