DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Clipboard API in Firefox: Async Copy/Paste, Rich Content, Images, and Permission Model

Clipboard handling in browsers is more complex than it appears. Firefox has specific behavior around clipboard permissions, async clipboard API, and cross-origin restrictions that web developers need to understand.

The Clipboard Permission Model

Reading from clipboard: Requires user permission in Firefox.

Modern clipboard API:

// Read text (requires clipboard-read permission in most cases)
const text = await navigator.clipboard.readText();

// Write text (usually allowed without explicit permission prompt)
await navigator.clipboard.writeText('Hello, clipboard!');
Enter fullscreen mode Exit fullscreen mode

Firefox vs Chrome permission difference:

  • Chrome: clipboard-read permission is gated
  • Firefox: navigator.clipboard.readText() may prompt or may require focus

In practice: Writing to clipboard (writeText) is allowed automatically after a user gesture (click, key press). Reading from clipboard requires explicit user permission in newer Firefox versions.

The Async Clipboard API

The modern API (navigator.clipboard) is promise-based:

// Copy button handler
document.getElementById('copy-btn').addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText('Content to copy');
    console.log('Copied!');
  } catch (err) {
    console.error('Copy failed:', err);
  }
});
Enter fullscreen mode Exit fullscreen mode

HTTPS requirement: The Clipboard API requires HTTPS (or localhost). On plain HTTP, navigator.clipboard is undefined.

// Safe check
if (navigator.clipboard) {
  await navigator.clipboard.writeText(text);
} else {
  // Fallback for HTTP or unsupported browsers
  fallbackCopy(text);
}
Enter fullscreen mode Exit fullscreen mode

The Old document.execCommand('copy') Fallback

Before the async Clipboard API, execCommand('copy') was standard. It still works in Firefox:

function copyToClipboard(text) {
  const el = document.createElement('textarea');
  el.value = text;
  el.setAttribute('readonly', '');
  el.style.position = 'absolute';
  el.style.left = '-9999px';
  document.body.appendChild(el);
  el.select();
  document.execCommand('copy');
  document.body.removeChild(el);
}
Enter fullscreen mode Exit fullscreen mode

When to use this: When you need to support HTTP contexts or older Firefox versions. It's considered deprecated but still functional.

Important: execCommand('copy') requires a user gesture (must be called from a click handler, not an async callback chain that broke the user gesture link).

Copy Rich Content (HTML and Text)

The newer ClipboardItem API handles rich content:

// Copy HTML + plain text fallback
const htmlContent = '<strong>Bold text</strong>';
const textContent = 'Bold text';

const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
const textBlob = new Blob([textContent], { type: 'text/plain' });

await navigator.clipboard.write([
  new ClipboardItem({
    'text/html': htmlBlob,
    'text/plain': textBlob
  })
]);
Enter fullscreen mode Exit fullscreen mode

Firefox support for ClipboardItem: Firefox 87+ supports ClipboardItem. The pasting application decides which format to use.

Test it: Copy rich content via this API, paste into a rich text editor (Google Docs, Word) — should paste formatted. Paste into a plain text editor — should paste plain text.

Copy Images

// Copy a canvas as an image to clipboard
async function copyCanvasToClipboard(canvas) {
  canvas.toBlob(async (blob) => {
    await navigator.clipboard.write([
      new ClipboardItem({ 'image/png': blob })
    ]);
  });
}

// Or from a URL (fetch the image first)
const response = await fetch('/image.png');
const blob = await response.blob();
await navigator.clipboard.write([
  new ClipboardItem({ 'image/png': blob })
]);
Enter fullscreen mode Exit fullscreen mode

Firefox support: Copy image via Clipboard API works in Firefox 87+.

Clipboard Read: Reading What's Pasted

For reading clipboard content (paste functionality):

// Read text from clipboard (requires user permission)
document.addEventListener('paste', async (event) => {
  const items = event.clipboardData.items;
  for (const item of items) {
    if (item.type === 'text/plain') {
      const text = await new Promise(resolve => item.getAsString(resolve));
      console.log('Pasted text:', text);
    }
    if (item.type.startsWith('image/')) {
      const file = item.getAsFile();
      // Handle pasted image
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Using the Clipboard API for read:

// Read plain text (triggers permission prompt)
const text = await navigator.clipboard.readText();

// Read clipboard contents
const items = await navigator.clipboard.read();
for (const item of items) {
  if (item.types.includes('text/plain')) {
    const blob = await item.getType('text/plain');
    const text = await blob.text();
    console.log(text);
  }
}
Enter fullscreen mode Exit fullscreen mode

Firefox behavior: Firefox will prompt for clipboard permission when readText() is called outside of a paste event handler.

Debugging Clipboard in Firefox

In DevTools Console:

// Test clipboard write
await navigator.clipboard.writeText('test');
// Then paste somewhere to verify

// Test clipboard read (will prompt)
const text = await navigator.clipboard.readText();
console.log(text);
Enter fullscreen mode Exit fullscreen mode

Check permission status:

const permission = await navigator.permissions.query({ name: 'clipboard-read' });
console.log('Clipboard read:', permission.state); // 'granted', 'denied', 'prompt'
Enter fullscreen mode Exit fullscreen mode

Common Clipboard Bugs in Firefox

Bug: Copy fails silently:
Most common cause: writeText() called outside a user gesture context. An async operation that called writeText after a timeout or fetch — the user gesture context was lost.

// BREAKS: gesture context lost after await
button.addEventListener('click', async () => {
  const data = await fetchSomeData();  // context lost here
  await navigator.clipboard.writeText(data);  // fails silently or with error
});

// FIX: write before the async operation
button.addEventListener('click', async () => {
  const textPromise = fetchSomeData();  // start fetch
  // Write placeholder immediately (in gesture context)
  await navigator.clipboard.writeText('Loading...');
  const data = await textPromise;
  // Update clipboard
  await navigator.clipboard.writeText(data);
});
Enter fullscreen mode Exit fullscreen mode

Or use ClipboardItem with a Promise:

button.addEventListener('click', () => {
  // Pass a Promise to ClipboardItem — keeps gesture context valid
  navigator.clipboard.write([
    new ClipboardItem({
      'text/plain': fetchSomeData().then(data => 
        new Blob([data], { type: 'text/plain' })
      )
    })
  ]);
});
Enter fullscreen mode Exit fullscreen mode

Bug: Different behavior on HTTP vs HTTPS:
navigator.clipboard is undefined on HTTP. Always check before using.

Bug: iOS Firefox:
iOS has additional restrictions from Safari's WebKit. Clipboard API behavior on Firefox iOS inherits WebKit's restrictions.

Extension Clipboard Access

Firefox extensions have special clipboard access:

// In a Firefox extension background script:
// No permission needed for copy
browser.tabs.executeScript({ code: `document.execCommand('copy')` });

// For full clipboard access, request the permission in manifest.json:
// "permissions": ["clipboardRead", "clipboardWrite"]
Enter fullscreen mode Exit fullscreen mode

With the clipboardRead and clipboardWrite permissions, extensions bypass the user gesture requirement and can read/write clipboard programmatically.

Tridactyl uses this for its yy (yank current URL) and P (paste and open URL) commands.

Top comments (0)