clipboard

Clipboard API — Live Demos

checking…

Copy text with feedback — writeText()

The classic copy button. Real navigator.clipboard.writeText() — try it, then paste anywhere (Ctrl/Cmd+V) to confirm. The "Copied!" state is essential UX.

npm install w3tweaks

Or copy your own text:

copyBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(text); showCopied(); // "Copied!" for 2s } catch (err) { fallbackCopy(text); } });
--:--:--Click Copy, then paste somewhere to confirm it worked

Copy a generated image — write() + ClipboardItem

Draw on the canvas, then copy it as a PNG. Uses canvas.toBlob()new ClipboardItem({'image/png': blob})clipboard.write(). Paste into an image editor or doc to confirm.

--:--:--Draw, then copy the canvas as a PNG image

Read the clipboard — readText() & read()

Reading is more restricted than writing (permission prompt, focus required). Click to read text, or paste an image into the drop zone with Ctrl/Cmd+V.

Read result appears here · or focus this box and press Ctrl/Cmd+V to paste an image
Reading is gated: Chromium prompts for permission + needs focus · Safari shows a paste callout · clipboard-read permission doesn't exist in Firefox/Safari. Never read on page load.
--:--:--Copy some text elsewhere, then click "Read text"

One item, multiple formats — HTML + plain text

A single ClipboardItem can carry both text/html and text/plain. Rich editors get the HTML; plain fields get the text. Copy below, then paste into a rich editor vs a plain text field.

text/html text/plain
Rich editors paste this (text/html)
W3Tweaksadvanced HTML tutorials
Plain fields paste this (text/plain)
W3Tweaks — advanced HTML tutorials
new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }), 'text/plain': new Blob([plain], { type: 'text/plain' }) });
--:--:--Copy both, then paste into a rich editor vs a plain text field

The Safari async-write trap — and the fix

Pick a browser engine to simulate. The left awaits a fetch before write() (breaks activation in Safari). The right passes a promise as the ClipboardItem value (works everywhere).

Simulate engine:
❌ await fetch() then write()
No result yet
✅ promise as ClipboardItem value
No result yet
const blob = await fetch(url).then(r => r.blob()); // await breaks activation await navigator.clipboard.write([new ClipboardItem({'image/png': blob})]); await navigator.clipboard.write([new ClipboardItem({ 'image/png': fetch(url).then(r => r.blob()) // promise as value })]); // write() called synchronously → gesture intact
--:--:--Set engine to Safari, then run both versions to see the difference