To copy text to the clipboard in JavaScript, call the async Clipboard API: await navigator.clipboard.writeText('text to copy'). It returns a Promise, works in every modern browser, and replaces the deprecated document.execCommand('copy'). The only catches: the page must run in a secure context (HTTPS or localhost) and the copy should be triggered by a user action such as a click.
Key takeaways
- Use
navigator.clipboard.writeText(text)to copy andnavigator.clipboard.readText()to read — both return Promises. - The Clipboard API requires a secure context (HTTPS or
localhost) and is usually gated behind a user gesture (click or keypress). document.execCommand('copy')is deprecated but still works as a fallback for very old browsers via a hidden<textarea>.- Wrap calls in
try/catchwithasync/awaitso you can show clear success or error feedback. - For images, HTML, or multiple formats, use
navigator.clipboard.write()with aClipboardItem. - Skip dead, Flash-based libraries like ZeroClipboard — the native API now covers nearly every case.
How to copy text to the clipboard in JavaScript
The modern approach is the asynchronous Clipboard API, exposed on navigator.clipboard. The writeText() method takes a string, writes it to the system clipboard, and returns a Promise that resolves when the copy succeeds or rejects if it fails (for example, when the page is not in a secure context).
Because it is Promise-based, pair it with async/await and a try/catch block:
/**
* Copy a string to the system clipboard.
* @param {string} text - The text to copy.
* @returns {Promise<boolean>} Resolves true on success, false on failure.
*/
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Copy failed:', err);
return false;
}
}
// Trigger it from a user action (required by most browsers):
document.querySelector('#copy-btn').addEventListener('click', () => {
copyText('Hello from the Clipboard API!');
});Building a copy button with success and error feedback
In a real UI you want to tell the user what happened. The pattern below reads text from an element, copies it, and briefly swaps the button label so the user gets visual confirmation — all driven by a single click handler.
/**
* Wire up a "copy to clipboard" button with inline feedback.
* @param {string} buttonSelector - CSS selector for the trigger button.
* @param {string} sourceSelector - CSS selector for the element to copy from.
*/
function setupCopyButton(buttonSelector, sourceSelector) {
const button = document.querySelector(buttonSelector);
const source = document.querySelector(sourceSelector);
const original = button.textContent;
button.addEventListener('click', async () => {
const text = source.value ?? source.textContent;
try {
await navigator.clipboard.writeText(text);
button.textContent = 'Copied!';
} catch (err) {
button.textContent = 'Press Ctrl+C';
console.error('Clipboard write failed:', err);
} finally {
setTimeout(() => (button.textContent = original), 1500);
}
});
}
setupCopyButton('#copy-btn', '#snippet');Clipboard API vs document.execCommand('copy')
For years the only way to copy programmatically was document.execCommand('copy'). It is now deprecated: it is synchronous (blocking), gives no reliable success signal, and behaves differently across browsers. The async Clipboard API was designed to replace it.
| Feature | navigator.clipboard (Clipboard API) |
document.execCommand('copy') |
|---|---|---|
| Status | Standard, recommended | Deprecated (legacy only) |
| Sync / async | Asynchronous, Promise-based | Synchronous, blocks the main thread |
| Success signal | Promise resolves / rejects | Returns a boolean (often unreliable) |
| Secure context | Required (HTTPS / localhost) |
Worked on plain HTTP too |
| Permissions | Governed by the Permissions API | None |
| Rich data (images / HTML) | Yes, via ClipboardItem |
Text / selection only |
| Support in 2026 | All evergreen browsers | Everywhere, but unmaintained |
Why execCommand is deprecated
execCommand was never specified for reliability: it can return true even when nothing was copied, it forces you to select a DOM node first, and its behaviour varies by browser. The Clipboard API fixes all three by being explicit, Promise-based, and permission-aware. Treat execCommand strictly as a fallback, never as your primary path.
Copying with a fallback for very old browsers
If you must support legacy browsers, detect the Clipboard API and fall back to a hidden <textarea> plus execCommand('copy'). The helper below tries the modern API first and only drops to the legacy path when needed.
/**
* Copy text using the Clipboard API, falling back to execCommand on old browsers.
* @param {string} text - The text to copy.
* @returns {Promise<boolean>} True if the copy succeeded.
*/
async function copyWithFallback(text) {
// Modern path: async Clipboard API (secure context required).
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
/* fall through to the legacy path */
}
}
// Legacy fallback: hidden textarea + deprecated execCommand.
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
let ok = false;
try {
ok = document.execCommand('copy'); // deprecated; fallback only
} catch {
ok = false;
}
document.body.removeChild(textarea);
return ok;
}Permissions, secure contexts, and user gestures
The Clipboard API is privacy-sensitive, so browsers guard it:
- Secure context —
navigator.clipboardisundefinedon plain HTTP. Serve over HTTPS (or uselocalhostin development). - User gesture —
writeText()should run inside a user-initiated event handler (a click or keypress). Calling it on page load is usually blocked. - Permissions API — writing (
clipboard-write) is generally granted automatically, while reading (clipboard-read) typically prompts the user. Inspect the state withnavigator.permissions.query({ name: 'clipboard-read' }).
Reading from the clipboard and the paste event
To read text, call navigator.clipboard.readText() (this usually triggers a permission prompt). For paste-driven flows you can also listen for the paste event and read from event.clipboardData, which is handy for capturing pasted images or formatted text.
/**
* Read plain text from the clipboard (may prompt the user for permission).
* @returns {Promise<string>} The clipboard text, or '' on failure.
*/
async function pasteText() {
try {
return await navigator.clipboard.readText();
} catch (err) {
console.error('Read failed:', err);
return '';
}
}
// Alternative: capture content as the user pastes into the page.
document.addEventListener('paste', (event) => {
const pasted = event.clipboardData.getData('text/plain');
console.log('Pasted:', pasted);
});Copying images and rich content with ClipboardItem
For anything beyond plain text — PNG images, HTML, or several formats at once — use navigator.clipboard.write() with one or more ClipboardItem objects. Each ClipboardItem maps MIME types to Blob data, so the receiving app can pick the format it understands.
/**
* Copy a PNG image Blob to the clipboard.
* @param {Blob} pngBlob - A PNG image blob.
* @returns {Promise<void>}
*/
async function copyImage(pngBlob) {
const item = new ClipboardItem({ 'image/png': pngBlob });
await navigator.clipboard.write([item]);
}
// Copy both HTML and a plain-text fallback in one operation:
async function copyRichText() {
const html = new Blob(['<b>Bold text</b>'], { type: 'text/html' });
const plain = new Blob(['Bold text'], { type: 'text/plain' });
await navigator.clipboard.write([
new ClipboardItem({ 'text/html': html, 'text/plain': plain }),
]);
}Putting it together
For the vast majority of cases, navigator.clipboard.writeText() inside a click handler with a try/catch is all you need; add the execCommand fallback only if your analytics show meaningful legacy traffic. Skip Flash-era libraries like ZeroClipboard entirely — they no longer work in any current browser.
If you are wiring up clipboard behaviour alongside other front-end interactions, these guides pair well: drag-and-drop file uploads with Dropzone.js, event delegation patterns in jQuery, and where jQuery fits in a modern stack. For build tooling, see using Gulp.js to minify CSS and JS.
Need a polished, accessible front end built around interactions like these? Explore our web development services.
Frequently Asked Questions
How do I copy text to the clipboard in JavaScript?
Call await navigator.clipboard.writeText('your text') inside a click handler. It returns a Promise, so wrap it in try/catch to handle success and failure. The page must be served over HTTPS or run on localhost.
Why is document.execCommand('copy') deprecated?
document.execCommand('copy') is synchronous, gives an unreliable success signal, requires selecting a DOM node first, and behaves inconsistently across browsers. The async Clipboard API replaces it with a Promise-based, permission-aware design. Keep execCommand only as a fallback for very old browsers.
Does the Clipboard API work without HTTPS?
No. navigator.clipboard is only available in a secure context, meaning HTTPS or localhost. On plain HTTP it is undefined, so serve your site over HTTPS in production or provide an execCommand fallback.
Do I need permission to copy to the clipboard?
Writing text (clipboard-write) is generally granted automatically when the action follows a user gesture such as a click. Reading (clipboard-read) usually prompts the user. You can check the current state with navigator.permissions.query().
How do I copy an image to the clipboard?
Use navigator.clipboard.write() with a ClipboardItem, for example new ClipboardItem({ 'image/png': blob }). This lets you copy images, HTML, or multiple MIME types at once, which plain-text writeText() cannot do.
Why isn't my copy button working?
The most common causes are: the page is not on HTTPS or localhost, the copy is not triggered by a user gesture, or you forgot to await the Promise. Check the console for a rejected Promise and confirm window.isSecureContext is true.