JavaScript Clipboard API: Copy & Paste Without execCommand
AI generated
JS
() =>
JavaScript · Clipboard API · Copy & Paste · Browser APIs
JavaScript Clipboard API
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.

10 min read navigator.clipboard · writeText · readText · ClipboardItem Chrome · Firefox · Safari · Edge

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?
Synchronous, no reliable error handling, requires DOM manipulation. Marked legacy in WHATWG, being phased out by browsers.
2Why only on HTTPS?
Clipboard API is part of the Secure Context API. On HTTP, reading the clipboard via man-in-the-middle would be conceivable. Localhost without HTTPS is allowed.
3Do I need a permission for writeText()?
No, an active user gesture is enough. 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?
Check 'clipboard' in navigator. Fall back to the execCommand textarea trick. On error, prompt the user to copy manually.
9Clipboard API in iframes?
Only with allow="clipboard-read clipboard-write" on the iframe element. Without access, clipboard operations fail.
10Mock clipboard in tests?
Playwright: context.grantPermissions(['clipboard-read', 'clipboard-write']). In unit tests, replace navigator.clipboard with vi.spyOn or a Jest mock.