Clipboard API
The Clipboard API provides programmatic access to the system clipboard. It allows you to read and write text, HTML, images, and other data types to and from the clipboard. The API is part of the W3C Clipboard API specification and is available in all modern browsers.
Unlike the older document.execCommand('copy') approach, the Clipboard API is Promise-based, works asynchronously, and requires explicit user permission. It is the recommended way to handle clipboard operations in modern web applications.
| 1 | // Clipboard API overview |
| 2 | // Write to clipboard |
| 3 | await navigator.clipboard.writeText("Hello, clipboard!"); |
| 4 | |
| 5 | // Read from clipboard |
| 6 | const text = await navigator.clipboard.readText(); |
| 7 | console.log(text); // "Hello, clipboard!" |
| 8 | |
| 9 | // Check if Clipboard API is available |
| 10 | if (navigator.clipboard) { |
| 11 | console.log("Clipboard API available"); |
| 12 | } else { |
| 13 | console.log("Clipboard API not supported"); |
| 14 | } |
The writeText() method copies a string to the clipboard. It returns a Promise that resolves when the copy operation is complete. The method requires the page to be focused and the user to have triggered the action via a user gesture (click, keydown, etc.).
| 1 | // Basic copy to clipboard |
| 2 | async function copyText(text) { |
| 3 | try { |
| 4 | await navigator.clipboard.writeText(text); |
| 5 | console.log("Copied to clipboard!"); |
| 6 | return true; |
| 7 | } catch (err) { |
| 8 | console.error("Failed to copy:", err); |
| 9 | return false; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | // Usage in a click handler |
| 14 | document.getElementById("copy-btn").addEventListener("click", async () => { |
| 15 | const success = await copyText("Hello, World!"); |
| 16 | if (success) { |
| 17 | showNotification("Copied!"); |
| 18 | } |
| 19 | }); |
| 20 | |
| 21 | // Copy code block content |
| 22 | document.querySelectorAll("pre code").forEach((codeBlock) => { |
| 23 | codeBlock.addEventListener("click", async () => { |
| 24 | const text = codeBlock.textContent; |
| 25 | try { |
| 26 | await navigator.clipboard.writeText(text); |
| 27 | codeBlock.classList.add("copied"); |
| 28 | setTimeout(() => codeBlock.classList.remove("copied"), 2000); |
| 29 | } catch (err) { |
| 30 | console.error("Copy failed:", err); |
| 31 | } |
| 32 | }); |
| 33 | }); |
| 34 | |
| 35 | // Copy with feedback |
| 36 | async function copyWithFeedback(text, button) { |
| 37 | const originalText = button.textContent; |
| 38 | try { |
| 39 | await navigator.clipboard.writeText(text); |
| 40 | button.textContent = "Copied!"; |
| 41 | button.disabled = true; |
| 42 | setTimeout(() => { |
| 43 | button.textContent = originalText; |
| 44 | button.disabled = false; |
| 45 | }, 2000); |
| 46 | } catch (err) { |
| 47 | button.textContent = "Failed!"; |
| 48 | setTimeout(() => { |
| 49 | button.textContent = originalText; |
| 50 | }, 2000); |
| 51 | } |
| 52 | } |
The readText() method reads text from the clipboard. It requires explicit user permission — the browser will show a permission prompt if the user has not already granted access. This method requires the page to be focused.
| 1 | // Read text from clipboard |
| 2 | async function pasteText() { |
| 3 | try { |
| 4 | const text = await navigator.clipboard.readText(); |
| 5 | console.log("Pasted:", text); |
| 6 | return text; |
| 7 | } catch (err) { |
| 8 | if (err.name === "NotAllowedError") { |
| 9 | console.log("Permission denied — user may need to grant clipboard access"); |
| 10 | } else { |
| 11 | console.error("Paste failed:", err); |
| 12 | } |
| 13 | return null; |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | // Paste into an input field |
| 18 | document.getElementById("paste-btn").addEventListener("click", async () => { |
| 19 | const text = await pasteText(); |
| 20 | if (text !== null) { |
| 21 | document.getElementById("input-field").value = text; |
| 22 | } |
| 23 | }); |
| 24 | |
| 25 | // Paste on Ctrl+V (modern approach) |
| 26 | document.addEventListener("keydown", async (e) => { |
| 27 | if (e.ctrlKey && e.key === "v") { |
| 28 | // Note: this event fires before the native paste |
| 29 | // For most cases, let the browser handle the paste event |
| 30 | } |
| 31 | }); |
| 32 | |
| 33 | // Better: use the paste event |
| 34 | document.getElementById("text-area").addEventListener("paste", async (e) => { |
| 35 | e.preventDefault(); |
| 36 | try { |
| 37 | const text = await navigator.clipboard.readText(); |
| 38 | document.execCommand("insertText", false, text); |
| 39 | } catch (err) { |
| 40 | // Fallback: let browser handle it |
| 41 | console.log("Custom paste failed, using default behavior"); |
| 42 | } |
| 43 | }); |
info
The read() and write() methods handle multiple data types (text, HTML, images, files) using ClipboardItem objects. They provide more flexibility than the text-only methods.
| 1 | // Read multiple types from clipboard |
| 2 | async function readClipboard() { |
| 3 | try { |
| 4 | const clipboardItems = await navigator.clipboard.read(); |
| 5 | |
| 6 | for (const item of clipboardItems) { |
| 7 | // Check available types |
| 8 | for (const type of item.types) { |
| 9 | console.log("Available type:", type); |
| 10 | |
| 11 | if (type === "text/plain") { |
| 12 | const blob = await item.getType(type); |
| 13 | const text = await blob.text(); |
| 14 | console.log("Text:", text); |
| 15 | } |
| 16 | |
| 17 | if (type === "text/html") { |
| 18 | const blob = await item.getType(type); |
| 19 | const html = await blob.text(); |
| 20 | console.log("HTML:", html); |
| 21 | } |
| 22 | |
| 23 | if (type === "image/png") { |
| 24 | const blob = await item.getType(type); |
| 25 | const url = URL.createObjectURL(blob); |
| 26 | console.log("Image URL:", url); |
| 27 | // Display the image |
| 28 | const img = document.createElement("img"); |
| 29 | img.src = url; |
| 30 | document.body.appendChild(img); |
| 31 | } |
| 32 | } |
| 33 | } |
| 34 | } catch (err) { |
| 35 | console.error("Read failed:", err); |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | // Write rich content to clipboard |
| 40 | async function writeRichContent() { |
| 41 | const html = "<strong>Bold text</strong> and <em>italic</em>"; |
| 42 | const plainText = "Bold text and italic"; |
| 43 | |
| 44 | const blob = new Blob([html], { type: "text/html" }); |
| 45 | const textBlob = new Blob([plainText], { type: "text/plain" }); |
| 46 | |
| 47 | const item = new ClipboardItem({ |
| 48 | "text/html": blob, |
| 49 | "text/plain": textBlob |
| 50 | }); |
| 51 | |
| 52 | await navigator.clipboard.write([item]); |
| 53 | console.log("Rich content copied!"); |
| 54 | } |
The Clipboard API requires specific permissions. writeText() works without explicit permission (must be triggered by user gesture). readText() and read() require user permission. You can check permission status using the Permissions API.
| 1 | // Check clipboard read permission |
| 2 | async function checkClipboardPermission() { |
| 3 | try { |
| 4 | const result = await navigator.permissions.query({ |
| 5 | name: "clipboard-read" |
| 6 | }); |
| 7 | |
| 8 | console.log("Permission state:", result.state); |
| 9 | // "granted", "denied", or "prompt" |
| 10 | |
| 11 | if (result.state === "granted") { |
| 12 | const text = await navigator.clipboard.readText(); |
| 13 | console.log("Clipboard:", text); |
| 14 | } else if (result.state === "prompt") { |
| 15 | // Will prompt user on next read attempt |
| 16 | console.log("User will be prompted for permission"); |
| 17 | } else { |
| 18 | console.log("Clipboard access denied"); |
| 19 | } |
| 20 | } catch (err) { |
| 21 | // Permissions API may not support clipboard-read |
| 22 | console.log("Cannot check permission:", err.message); |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | // Safe clipboard read with fallback |
| 27 | async function safeReadClipboard() { |
| 28 | try { |
| 29 | const text = await navigator.clipboard.readText(); |
| 30 | return text; |
| 31 | } catch (err) { |
| 32 | if (err.name === "NotAllowedError") { |
| 33 | console.log("Clipboard permission denied — showing manual paste UI"); |
| 34 | showManualPasteDialog(); |
| 35 | } else if (err.name === "NotFoundError") { |
| 36 | console.log("Clipboard is empty"); |
| 37 | return ""; |
| 38 | } else { |
| 39 | throw err; |
| 40 | } |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | function showManualPasteDialog() { |
| 45 | const dialog = document.createElement("div"); |
| 46 | dialog.innerHTML = ` |
| 47 | <p>Press Ctrl+V (Cmd+V) to paste:</p> |
| 48 | <textarea id="manual-paste" rows="4"></textarea> |
| 49 | <button id="manual-paste-ok">OK</button> |
| 50 | `; |
| 51 | document.body.appendChild(dialog); |
| 52 | } |
Not all browsers support the Clipboard API, and some environments (like iframes) restrict clipboard access. Always provide fallback strategies using the older document.execCommand() approach or manual paste prompts.
| 1 | // Fallback using execCommand (deprecated but widely supported) |
| 2 | function copyTextFallback(text) { |
| 3 | const textarea = document.createElement("textarea"); |
| 4 | textarea.value = text; |
| 5 | textarea.style.position = "fixed"; |
| 6 | textarea.style.left = "-9999px"; |
| 7 | textarea.style.top = "-9999px"; |
| 8 | document.body.appendChild(textarea); |
| 9 | textarea.focus(); |
| 10 | textarea.select(); |
| 11 | |
| 12 | try { |
| 13 | const success = document.execCommand("copy"); |
| 14 | document.body.removeChild(textarea); |
| 15 | return success; |
| 16 | } catch (err) { |
| 17 | document.body.removeChild(textarea); |
| 18 | return false; |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | // Modern API with fallback |
| 23 | async function copyTextSmart(text) { |
| 24 | // Try modern API first |
| 25 | if (navigator.clipboard && window.isSecureContext) { |
| 26 | try { |
| 27 | await navigator.clipboard.writeText(text); |
| 28 | return true; |
| 29 | } catch (err) { |
| 30 | console.warn("Modern clipboard failed, falling back:", err); |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | // Fallback to execCommand |
| 35 | return copyTextFallback(text); |
| 36 | } |
| 37 | |
| 38 | // Paste event handler (always works — no permission needed) |
| 39 | document.addEventListener("paste", (e) => { |
| 40 | const text = e.clipboardData?.getData("text/plain"); |
| 41 | if (text) { |
| 42 | console.log("Pasted:", text); |
| 43 | } |
| 44 | }); |
| 45 | |
| 46 | // Copy event handler (for intercepting Ctrl+C) |
| 47 | document.addEventListener("copy", (e) => { |
| 48 | const selection = window.getSelection()?.toString(); |
| 49 | if (selection) { |
| 50 | console.log("User copied:", selection); |
| 51 | // Let the default copy happen, or override with: |
| 52 | // e.clipboardData.setData("text/plain", customText); |
| 53 | // e.preventDefault(); |
| 54 | } |
| 55 | }); |
best practice