Copy & Paste Without execCommand
execCommand('copy') was a hack that is now marked deprecated in modern browsers. The Clipboard API delivers a clean, Promise-based interface for reading and writing the clipboard, including image and rich text support plus a built-in permission model.
Table of Contents
- 1. The Problem With execCommand
- 2. The Clipboard API: Overview and Availability
- 3. Writing Text to the Clipboard
- 4. Reading Text From the Clipboard
- 5. Permissions With the Permissions API
- 6. ClipboardItem: Images and Rich Text
- 7. Clipboard Events: copy, cut and paste
- 8. Fallbacks for Older Browsers and Edge Cases
- 9. Clipboard API vs. execCommand Compared
- 10. Summary
- 11. FAQ
1. The Problem With execCommand
document.execCommand('copy') was, for years, the only way to copy text to the clipboard programmatically. The procedure is cumbersome: you have to create an invisible textarea element, insert the text, attach it to the DOM, select it, call execCommand and then remove the element again. In the process, execCommand can fail silently on some browsers and in certain contexts, without an error message. The return value false signals failure but gives no reason for it. No Promise, no await, no reliable error handling.
The Clipboard API solves all of these problems. It is Promise-based, can be used with async/await, throws real exceptions on failure and is part of an official W3C specification. The old execCommand method is marked "legacy" in the WHATWG specification and is being phased out in some browsers. Anyone building new clipboard functionality today should rely exclusively on the Clipboard API via navigator.clipboard, and keep a targeted fallback ready for older browsers.
2. The Clipboard API: Overview and Availability
The Clipboard API is accessible through the navigator.clipboard object. It provides four core methods: writeText(text) writes a string to the clipboard, readText() reads the text content of the clipboard, write(items) writes arbitrary MIME types including images, and read() reads all current clipboard contents. All four methods return Promises. The Clipboard API is available in all modern browsers: Chrome from version 66, Firefox from 63, Safari from 13.1, Edge from 79.
An important security concept of the Clipboard API: it only works in secure contexts (HTTPS or localhost). On HTTP pages, navigator.clipboard is undefined. The specification also distinguishes between write and read access: writing (writeText, write) is automatically allowed in many browsers as long as the code runs inside a user gesture handler (for example a click event). Reading (readText, read), on the other hand, requires explicit user permission, either through the browser's permission dialog or through the browser window having focus combined with an active user gesture.
// Guard: Check if Clipboard API is available (requires HTTPS)
function isClipboardAvailable() {
return (
'clipboard' in navigator &&
typeof navigator.clipboard.writeText === 'function'
);
}
// Simple copy button, must be inside a user gesture handler
document.getElementById('copy-btn').addEventListener('click', async () => {
if (!isClipboardAvailable()) {
showToast('Clipboard not supported in this browser');
return;
}
try {
await navigator.clipboard.writeText('https://mironsoft.de');
showToast('Link copied!');
} catch (err) {
// DOMException: NotAllowedError if permission denied
console.error('Clipboard write failed:', err.name, err.message);
showToast('Copy failed, please copy manually');
}
});
3. Writing Text to the Clipboard
navigator.clipboard.writeText(text) is the simplest method of the Clipboard API. It returns a Promise that resolves when the write succeeds, or rejects with a DOMException error. The most common error is NotAllowedError, which occurs when the call happens outside a user gesture handler or the user has denied permission. The Promise allows clean error handling with try/catch instead of the fragile boolean return value of execCommand.
A practical pattern for the Clipboard API: a generic copy button that reads a data-clipboard-text attribute, copies the text, gives visual feedback (for example an icon swap or a toast message) and restores the original state after two seconds. This pattern can be implemented in a few lines of code without a JavaScript framework and works for every copy button on a page when you use event delegation.
// Generic copy button with visual feedback
document.addEventListener('click', async (event) => {
const btn = event.target.closest('[data-clipboard-text]');
if (!btn) return;
const text = btn.dataset.clipboardText;
const originalLabel = btn.textContent;
try {
await navigator.clipboard.writeText(text);
// Visual feedback, restore after 2 seconds
btn.textContent = 'Copied!';
btn.setAttribute('aria-label', 'Copied to clipboard');
btn.disabled = true;
setTimeout(() => {
btn.textContent = originalLabel;
btn.removeAttribute('aria-label');
btn.disabled = false;
}, 2000);
} catch {
btn.textContent = 'Failed';
setTimeout(() => { btn.textContent = originalLabel; }, 2000);
}
});
// Usage in HTML:
// <button data-clipboard-text="npm install mironsoft-ui">Copy</button>
4. Reading Text From the Clipboard
navigator.clipboard.readText() reads the current text content of the clipboard and returns it as a Promise. This method requires the clipboard-read permission and throws a NotAllowedError if it is denied. In practice, the browser opens a permission dialog on first use. Safari behaves differently here: it only allows readText() when the browser tab has focus and an active user gesture is present, without an explicit permission dialog.
Typical use cases for readText() with the Clipboard API are paste buttons in code editors that insert the clipboard content into an input field, or analysis tools that process pasted text directly. For simple pasting into a focused input field, the paste event with event.clipboardData.getData('text') is often the better choice, because it requires no explicit permission and is more broadly compatible.
5. Permissions With the Permissions API
The Clipboard API is closely tied to the Permissions API. With navigator.permissions.query({ name: 'clipboard-read' }) you can query the current permission status without triggering the permission dialog. Possible values are 'granted', 'denied' and 'prompt'. That lets you adapt UI elements accordingly: if permission is already granted, a paste button can be active immediately. If it is denied, show a hint on how the user can grant it. If it is 'prompt', show the button and trigger the dialog on click.
For clipboard-write, the status in Chrome is always 'granted' as long as an active user gesture is present, so permissions.query for clipboard-write is, in practice, less useful than for clipboard-read. Firefox does not yet fully support permissions.query for clipboard permissions, nor does Safari. For production-ready Clipboard API implementations, a defensive strategy is therefore recommended: always work with try/catch and never assume that a permission has been granted.
// Check clipboard-read permission before showing paste button
async function checkClipboardReadPermission() {
if (!('permissions' in navigator)) return 'unknown';
try {
const status = await navigator.permissions.query({ name: 'clipboard-read' });
// status.state: 'granted' | 'denied' | 'prompt'
return status.state;
} catch {
// Firefox: clipboard-read not recognized in permissions API
return 'unknown';
}
}
// Adaptive paste button initialization
async function initPasteButton(btn) {
const permission = await checkClipboardReadPermission();
if (permission === 'denied') {
btn.disabled = true;
btn.title = 'Clipboard access denied. Enable it in browser settings.';
return;
}
btn.addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
document.getElementById('input-field').value = text;
} catch (err) {
if (err.name === 'NotAllowedError') {
showPermissionHint();
}
}
});
}
6. ClipboardItem: Images and Rich Text
With navigator.clipboard.write([new ClipboardItem({...})]), the Clipboard API allows writing arbitrary MIME types. The classic example is copying canvas content as a PNG image. The process: convert the canvas content to a blob with canvas.toBlob(), wrap the blob in a ClipboardItem and write the item to the clipboard with navigator.clipboard.write(). The user can then paste the image directly into other applications (image editor, email client, Word).
Safari has a peculiarity here: it only supports ClipboardItem from Safari 13.1 onward and expects the blob generator to be a Promise that is initiated synchronously during the user gesture. That means you cannot create the blob after an await and then populate ClipboardItem, because the user gesture window has already closed by that point in Safari. The Safari-compatible solution passes a Promise for the blob directly to the ClipboardItem. The Clipboard API specification has explicitly allowed this since 2021.
// Copy canvas content as PNG image to clipboard
async function copyCanvasAsImage(canvas) {
if (!('ClipboardItem' in window)) {
throw new Error('ClipboardItem not supported, update your browser');
}
// Safari-compatible: pass a Promise directly to ClipboardItem
const blobPromise = new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error('Canvas toBlob failed'));
}, 'image/png');
});
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blobPromise }),
]);
}
// Copy both plain text and HTML to clipboard simultaneously
async function copyRichText(html, plainText) {
const htmlBlob = new Blob([html], { type: 'text/html' });
const textBlob = new Blob([plainText], { type: 'text/plain' });
await navigator.clipboard.write([
new ClipboardItem({
'text/html': htmlBlob,
'text/plain': textBlob,
}),
]);
}
// Usage
document.getElementById('copy-image-btn').addEventListener('click', async () => {
const canvas = document.querySelector('canvas');
try {
await copyCanvasAsImage(canvas);
showToast('Image copied to clipboard!');
} catch (err) {
console.error(err);
showToast('Copy failed: ' + err.message);
}
});
7. Clipboard Events: copy, cut and paste
Alongside the asynchronous Clipboard API via navigator.clipboard, there are the synchronous clipboard events copy, cut and paste. These events fire when the user performs the corresponding keyboard shortcuts or context menu actions. In the event handler, you have access to the clipboard via event.clipboardData and can read or modify its content before the browser runs its default action. event.preventDefault() stops the default action and gives you complete control.
The paste event with event.clipboardData.getData('text/plain') is more broadly compatible than navigator.clipboard.readText() and requires no explicit permission grant. It also works in older browsers and in Firefox without the clipboard-read permission dialog. That makes the event-based approach the preferred method when you only want to react to the user's paste actions, without actively reading the clipboard. The asynchronous Clipboard API, on the other hand, is the right choice when you need to write or read at an arbitrary point in time programmatically.
8. Fallbacks for Older Browsers and Edge Cases
Even though the Clipboard API is supported in all modern browsers, there are scenarios where fallbacks are necessary: HTTP pages (no HTTPS), older browser versions, browser extensions that block clipboard access, and headless browsers in tests. A robust fallback for writeText creates a textarea element, sets its value, attaches it hidden to the body, selects the content and calls document.execCommand('copy'). The element is then removed.
Another edge case: in Electron apps and certain WebView contexts, navigator.clipboard is present, but permissions behave differently than in the browser. Electron provides its own APIs (the clipboard module from the electron package) that are more reliable. For a production-ready library built around the Clipboard API, the following hierarchy is recommended: first try navigator.clipboard.writeText(), fall back to the execCommand fallback on error, and if that fails too, prompt the user to copy manually.
| Method | Browser Support | Permission Needed | HTTPS Needed |
|---|---|---|---|
| navigator.clipboard.writeText() | All modern browsers | No (user gesture is enough) | Yes |
| navigator.clipboard.readText() | All modern browsers | Yes (clipboard-read) | Yes |
| ClipboardItem (images) | Chrome, Edge, Safari 13.1+ | No (user gesture is enough) | Yes |
| clipboard events (paste) | All browsers incl. IE11 | No | No |
| execCommand('copy') | Deprecated, soon removed | No | No |
9. Clipboard API vs. execCommand Compared
The practical difference between the modern Clipboard API and execCommand becomes especially visible in error handling. execCommand('copy') returns false when it fails, without an error message, without an error type, without a stack trace. The Clipboard API throws a DOMException with a name (NotAllowedError, SecurityError) and a message. That makes debugging and context-aware error handling possible.
Another key difference: execCommand only works synchronously in the context of the active document and requires a selected DOM element. The Clipboard API is document-independent and can write any string or blob type without DOM manipulation. Going forward, the decision is clear: execCommand is on the deprecation list of every browser vendor, while the Clipboard API is being actively developed further. Anyone writing code today should rely exclusively on the asynchronous Clipboard API.
Mironsoft
Modern browser APIs, JavaScript development and web standards
Want to remove deprecated execCommand from your code?
We migrate existing copy-paste functionality to the modern Clipboard API, with robust error handling, browser fallbacks and full support for text, images and rich text.
Code Migration
Identify execCommand calls and replace them with the Clipboard API
Browser Compatibility
Safari edge cases, Firefox permissions and robust fallback strategies
Rich Text Support
ClipboardItem for images and HTML, including canvas export to the clipboard
10. Summary
The Clipboard API via navigator.clipboard is the modern, standards-compliant replacement for the outdated document.execCommand('copy'). It offers Promise-based methods for text (writeText, readText) and arbitrary MIME types (write with ClipboardItem), supports proper error handling with try/catch and integrates cleanly with the Permissions API. The main constraints are the HTTPS requirement and the permission requirement for readText(), both of which can be handled with a well-designed feature detection pattern and targeted fallbacks.
For new projects, the rule is: never use execCommand again. For existing codebases, migration is worthwhile, especially because execCommand is actively on the deprecation list in Chrome and Firefox. The Clipboard API also enables features that were not possible with execCommand: copying canvas content as images, writing multiple MIME types simultaneously, and reacting to the user's permission changes in real time.
Clipboard API: The Essentials at a Glance
Writing Text
await navigator.clipboard.writeText(text), called inside a user gesture handler. Promise-based, throws real exceptions. HTTPS required.
Reading Text
await navigator.clipboard.readText(), requires clipboard-read permission. Alternative: the paste event without permission.
Images and Rich Text
ClipboardItem with a blob, Safari expects the Promise directly in the constructor. Firefox support for ClipboardItem is limited.
Fallback
Feature detection: 'clipboard' in navigator. Fall back to execCommand for HTTP pages. On error: prompt the user to copy manually.
11. FAQ: JavaScript Clipboard API
1Why is execCommand deprecated?
2Why only on HTTPS?
3Do I need a permission for writeText()?
clipboard-write is granted by default in click handlers.4How do I copy an image?
ClipboardItem with a PNG blob from canvas.toBlob(). Safari expects the Promise directly in the constructor.5Read text without clipboard-read?
paste event: event.clipboardData.getData('text/plain'). No permission needed, works in all browsers.6Check permission?
navigator.permissions.query({ name: 'clipboard-read' }), returns 'granted', 'denied' or 'prompt'. Firefox/Safari incomplete.7Copy HTML text?
clipboard.write() with a ClipboardItem for text/html and text/plain simultaneously. Recipient chooses the preferred MIME type.8What if the Clipboard API is missing?
'clipboard' in navigator. Fall back to the execCommand textarea trick. On error, prompt the user to copy manually.9Clipboard API in iframes?
allow="clipboard-read clipboard-write" on the iframe element. Without access, clipboard operations fail.10Mock clipboard in tests?
context.grantPermissions(['clipboard-read', 'clipboard-write']). In unit tests, replace navigator.clipboard with vi.spyOn or a Jest mock.