· deepdives · 6 min read
Unlocking the Clipboard: Exploring the Async Clipboard API
Learn how to use the Async Clipboard API to read and write text, HTML, images and files securely from the browser. Practical examples, fallbacks, permissions and best practices included.

What you’ll build and why it matters
You will learn how to copy and paste richer data (not just plain text) between your web app and the system clipboard - safely, reliably, and with good UX. You’ll be able to: copy text and HTML, push images and files to the clipboard, read pasted images or files, and provide sensible fallbacks when the API isn’t available.
This isn’t just an API tour. It’s a practical guide with code you can paste into your projects right away. Use it to implement share buttons, image editors, clipboard-driven uploads, or improved copy/paste workflows in web apps.
Quick overview: what the Async Clipboard API gives you
- navigator.clipboard.writeText / readText - simple one-line copy/paste for plain text.
- navigator.clipboard.write / read - read and write arbitrary MIME types (HTML, images, custom types) using ClipboardItem objects.
- Programmatic copy of canvas images, files, and multi-format data.
The API is asynchronous (promises) and designed for secure contexts. Browser support varies, so feature-detect and provide fallbacks. See MDN for compatibility details: https://developer.mozilla.org/en-US/docs/Web/API/Async_Clipboard_API
Minimal examples - copy and paste text
Copy text (modern, simplest):
async function copyText(text) {
if (!navigator.clipboard) throw new Error('Clipboard API not available');
await navigator.clipboard.writeText(text);
}
// usage
copyText('Hello world')
.then(() => console.log('Copied!'))
.catch(err => console.error('Copy failed', err));Read text:
async function readText() {
if (!navigator.clipboard) throw new Error('Clipboard API not available');
return await navigator.clipboard.readText();
}
readText().then(text => console.log('Pasted:', text));These are the workhorses for most copy/paste needs. But there’s more: if you need HTML, images, or files, move on to ClipboardItem.
Advanced usage: copying HTML and multiple formats
Sometimes you want to copy rich HTML but provide a plain-text fallback too. Create a ClipboardItem containing multiple MIME types so paste targets can pick the best format.
async function copyHtmlWithPlainText(htmlString, plainText) {
if (!navigator.clipboard || !window.ClipboardItem) {
throw new Error('Rich clipboard not supported');
}
const blobHtml = new Blob([htmlString], { type: 'text/html' });
const blobText = new Blob([plainText], { type: 'text/plain' });
const item = new ClipboardItem({
'text/html': blobHtml,
'text/plain': blobText,
});
await navigator.clipboard.write([item]);
}When a user pastes into a rich editor, it will likely use text/html. When pasting into a plain text field, text/plain will be used.
Copying images (canvas → clipboard)
Copying images is where the Async Clipboard API shines. Example: copy a canvas image to the clipboard.
async function copyCanvasImage(canvas) {
if (!navigator.clipboard || !window.ClipboardItem) {
throw new Error('Image clipboard not supported');
}
const blob = await new Promise(resolve =>
canvas.toBlob(resolve, 'image/png')
);
const clipboardItem = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([clipboardItem]);
}This enables features like “Copy as PNG” in image editors and graphics apps built for the web.
Reading images and files from the clipboard
You can inspect clipboard contents and handle different types. This is how to read an image (if available):
async function readClipboardItems() {
if (!navigator.clipboard || !navigator.clipboard.read) {
throw new Error('Clipboard read not supported');
}
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
// Use blob: create URL or upload
const url = URL.createObjectURL(blob);
console.log('Found image in clipboard:', url);
// Remember to revokeObjectURL later
}
if (type === 'text/plain') {
const blob = await item.getType('text/plain');
const text = await blob.text();
console.log('Clipboard text:', text);
}
}
}
}Note: read() returns ClipboardItems; call getType to get a Blob for each MIME type.
Permissions, gestures and security considerations
- Secure context only: the Async Clipboard API is only available on HTTPS (or localhost).
- User gesture requirements: many browsers require a user gesture (click, keypress) for reading or writing. Write operations are generally more lenient than reads, but you must not rely on this across all browsers.
- Permissions API: you can query permissions for clipboard actions, though behavior differs between browsers. Example:
async function checkClipboardPermission(name) {
// name is 'clipboard-read' or 'clipboard-write'
if (!navigator.permissions) return 'unknown';
try {
const status = await navigator.permissions.query({ name });
return status.state; // 'granted' | 'denied' | 'prompt'
} catch (err) {
return 'unknown';
}
}- Privacy: reading the clipboard can leak sensitive data. Avoid surprise reads. Always indicate clearly why you’re requesting clipboard access and trigger reads from a deliberate user action.
References: MDN’s Async Clipboard API and the Permissions API: https://developer.mozilla.org/en-US/docs/Web/API/Async_Clipboard_API and https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API
Fallbacks and progressive enhancement
Not every browser supports read() / write() for rich types. Provide graceful fallbacks:
- For writeText: fall back to a temporary textarea + document.execCommand(‘copy’). See https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
- For readText when read() isn’t available: instruct the user to paste into a focused input/textarea and read its value.
- For images/files: if clipboard read isn’t supported, provide a drag-and-drop or file input fallback.
Example fallback for writeText:
async function copyTextWithFallback(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
// Fallback using textarea + execCommand
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
document.body.removeChild(ta);
}
}For paste fallback UI, offer a visible textarea with instructions like “Press Ctrl+V here” and then capture the pasted content.
Error handling and UX patterns
- Always wrap calls in try/catch. Clipboard operations can fail silently if permissions are blocked or user gesture rules aren’t met.
- Give the user actionable feedback: “Click to allow clipboard access” or “Press Ctrl+V into the box.” Don’t assume automatic permission.
- When writing files/images, inform users about successful copy (e.g., toast “Image copied to clipboard”).
Testing tips
- Test on real devices and browsers. Mobile browser clipboard behavior differs (and is often more restricted).
- Use browser feature detection, not user agent sniffing.
- For automated tests, you may need to stub navigator.clipboard in your test runner.
Performance and storage notes
Clipboard operations are usually fast, but reading large blobs can be memory-intensive. If you retrieve a blob to upload, stream or throttle processing where possible. Also revokeObjectURL for blobs you create with URL.createObjectURL when they’re no longer needed.
Example: a small copy-image button (complete)
<button id="copyImage">Copy Image</button>
<canvas id="c" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'orange';
ctx.fillRect(0, 0, 200, 200);
ctx.fillStyle = 'white';
ctx.fillRect(20, 20, 160, 160);
document.getElementById('copyImage').addEventListener('click', async () => {
try {
const blob = await new Promise(resolve =>
canvas.toBlob(resolve, 'image/png')
);
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
alert('Image copied to clipboard');
} catch (err) {
console.error('Copy failed', err);
alert('Could not copy image. Try saving instead.');
}
});
</script>This example shows the full flow: user gesture triggers canvas export, creation of ClipboardItem, and writing to the clipboard.
Common pitfalls and gotchas
- Not supported everywhere: check support before relying on read() / write() for rich types.
- Browser-specific permission prompts or policies. Some browsers disallow background clipboard reads altogether.
- Expect inconsistencies in how paste targets prefer one MIME type over another.
- Beware of overwriting user clipboard implicitly; only write when the user expects it.
Resources and further reading
- MDN: Async Clipboard API - https://developer.mozilla.org/en-US/docs/Web/API/Async_Clipboard_API
- MDN: ClipboardItem - https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem
- MDN: document.execCommand - https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
- web.dev guide: Async Clipboard API - https://web.dev/async-clipboard/
Final thoughts
The Async Clipboard API unlocks powerful UX patterns: programmatic copy of HTML, images, and files. It lets the browser be a true first-class editing environment instead of a text-only island. Use it to streamline workflows, but respect privacy and permissions. Start with writeText/readText for most needs, progressively enhance to ClipboardItem for richer experiences, and always provide fallbacks so your app works on every browser.



