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!'); 

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); } }); 

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); } 

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); } 

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 }) ]); 

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 }) ]); 

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 } } }); 

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); } } 

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); 

Check permission status:

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

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); }); 

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' }) ) }) ]); }); 

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"] 

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.