JavaScript Clipboard API
Learn the modern asynchronous JavaScript Clipboard API — copy and read text with navigator.clipboard, handle rich data with ClipboardItem.
The modern Clipboard API, exposed through navigator.clipboard, is the asynchronous, promise-based way to read from and write to the system clipboard. It replaces the old, synchronous document.execCommand('copy') approach with methods that return promises, so they pair naturally with async/await. The API is distinct from — but often used alongside — the cut, copy, and paste clipboard events: the events let you intercept what the user does, while navigator.clipboard lets your code initiate clipboard actions directly.
Writing Text to the Clipboard
The most common task is copying text. navigator.clipboard.writeText(text) accepts a string, writes it to the clipboard, and returns a Promise that resolves when the write succeeds and rejects when it fails.
Because it returns a promise, the cleanest way to use it is inside an async function with try...catch, so you can give the user feedback either way:
<button id="copyBtn">Copy</button>
<span id="status"></span>
<script>
const button = document.getElementById('copyBtn');
const status = document.getElementById('status');
button.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText('Hello from W3docs!');
status.textContent = 'Copied!';
} catch (err) {
status.textContent = 'Copy failed';
console.error('Clipboard write failed:', err);
}
});
</script>The await pauses until the clipboard write settles. If the user has denied permission or the page is not in a secure context, the promise rejects and the catch block runs — which is why you should never assume the copy succeeded.
Reading Text from the Clipboard
To read the clipboard's current text, call navigator.clipboard.readText(). It also returns a promise, this time resolving with the clipboard's text content:
<button id="pasteBtn">Read clipboard</button>
<p id="output"></p>
<script>
const button = document.getElementById('pasteBtn');
const output = document.getElementById('output');
button.addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
output.textContent = `Clipboard contains: ${text}`;
} catch (err) {
output.textContent = 'Could not read clipboard';
console.error('Clipboard read failed:', err);
}
});
</script>Reading is far more sensitive than writing, because it exposes whatever the user has copied — possibly a password or other private data. For that reason browsers guard readText() more strictly: it requires an explicit user gesture, and some browsers show a permission prompt the first time, or only allow reads when the page tab is focused.
Requirements and Gotchas
The Clipboard API has several rules that, if ignored, lead to silent rejections. Keep these in mind whenever you call it.
The Clipboard API only works in a secure context — that means HTTPS, or localhost during development. On a plain http:// page, navigator.clipboard is usually undefined. Calls also generally require a user gesture such as a click or key press, so triggering them on page load will fail. The Permissions API governs the clipboard-read and clipboard-write permissions, and reads may prompt the user. Because any call can reject — denied permission, an unfocused document, or an unsupported browser — always wrap clipboard calls in try...catch.
The page also needs focus for many clipboard operations. If you call readText() from, say, a setTimeout while the user has switched to another tab, expect a rejection with a "document is not focused" error. Note too that a clipboard read only happens after the user has focused and interacted with your page.
Copying Rich Data with ClipboardItem
Text is the easy case. To copy non-text data — images, HTML, or several formats at once — use navigator.clipboard.write(), which takes an array of ClipboardItem objects. Each ClipboardItem maps MIME types to their data (typically a Blob).
The example below fetches an image, wraps the resulting Blob in a ClipboardItem, and copies it:
async function copyImage(url) {
try {
const response = await fetch(url);
const blob = await response.blob();
const item = new ClipboardItem({ [blob.type]: blob });
await navigator.clipboard.write([item]);
console.log('Image copied to clipboard');
} catch (err) {
console.error('Failed to copy image:', err);
}
}The key inside the ClipboardItem constructor is the MIME type (here blob.type, e.g. 'image/png'), and the value is the data for that type. A single item can hold multiple representations — for instance both 'text/plain' and 'text/html' — letting the destination app pick the best one.
Reading rich data mirrors this with navigator.clipboard.read(), which resolves to an array of ClipboardItem objects you inspect by type:
async function readClipboardItems() {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
const blob = await item.getType(type);
console.log(`Found ${type}`, blob);
}
}
}Browser support for rich-data methods (write and read with ClipboardItem) is narrower and less consistent than for the text methods. Image MIME types in particular vary by browser — image/png is the most reliable. Feature-detect with if ('write' in navigator.clipboard) and fall back to copying text or a URL when rich data is unavailable.
The Legacy Fallback
Before the async API, copying meant selecting an element and calling document.execCommand('copy'). That method is now deprecated, but it still works in older browsers, so it is useful purely as a fallback. The typical pattern selected a hidden <textarea>, then ran the command:
function copyTextFallback(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy'); // deprecated
document.body.removeChild(textarea);
}Modern code should prefer the async API and only fall back when navigator.clipboard is missing:
async function copyText(text) {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
} else {
copyTextFallback(text);
}
}Real-World Uses
The Clipboard API powers many small but valuable interactions you see every day:
- "Copy code" buttons on documentation pages, so readers can grab a snippet without manual selection.
- "Copy link to share" buttons that put a URL on the clipboard for pasting into chat or email.
- Copying generated output, such as a password, an API key, or a formatted citation.
Whatever you copy, give visible feedback. A button that silently copies leaves users unsure whether it worked. Swap the label to "Copied!", show a brief toast, or update an adjacent status element — this is also an accessibility win, since screen-reader users get no clipboard cue from the browser itself.