"Copy clipboard" can mean three different things: pressing Ctrl+C to copy text you have selected, clicking a Copy to clipboard button on a web page, or writing JavaScript that copies clipboard content programmatically. All three end up in the same place — the system clipboard, a shared memory slot every application on your computer reads from — but they go through very different code paths, and each one has its own failure modes.
This guide covers all three. If you want to see exactly what lands on your clipboard after a copy, open Clipboard Inspector in a new tab, copy something, and press Ctrl+V on the page: every MIME type, format, and raw value is displayed instantly.
How to copy to the clipboard manually
On every major operating system the keyboard shortcut for copy is:
- Windows, Linux, ChromeOS:
Ctrl + C - macOS:
⌘ Command + C - iOS / iPadOS: Tap and hold the selection, then choose Copy from the popover
- Android: Tap and hold to enter selection mode, then tap Copy
Copying places multiple representations of the same content onto the clipboard at once — not just the text you see. A copy from a web page typically produces text/plain, text/html, and sometimes text/uri-list or application-specific MIME types. This is why the same paste can look different in a plain-text editor versus a rich-text one.
How to add a copy-to-clipboard button with JavaScript
The modern, recommended API is navigator.clipboard.writeText():
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text)
console.log('Copied to clipboard')
} catch (err) {
console.error('Copy failed', err)
}
}
document.querySelector('#copy-btn').addEventListener('click', () => {
copyToClipboard('Hello clipboard')
})Three rules to remember:
- HTTPS only.
navigator.clipboardis undefined on plain HTTP (exceptlocalhost). A copy button that works locally but fails on staging almost always has a TLS problem. - Must run from a user gesture. Browsers only allow clipboard writes inside an event handler triggered by a real user interaction — click, keydown, touchend. A copy triggered by
setTimeoutor a network callback is silently blocked. - Focus the document first. On some browsers, a copy attempted from an iframe without focus throws
NotAllowedError. Callwindow.focus()in the handler if you are copying from a non-active frame.
Copying rich content: HTML and images
To copy formatted HTML (so a rich-text editor can paste it with styles intact), use the lower-level navigator.clipboard.write() API with a ClipboardItem:
const html = '<p>Hello <strong>world</strong></p>'
const item = new ClipboardItem({
'text/html': new Blob([html], { type: 'text/html' }),
'text/plain': new Blob(['Hello world'], { type: 'text/plain' }),
})
await navigator.clipboard.write([item]) Always write a text/plain fallback alongside the HTML. Applications that do not understand text/html fall back to the plain version — omit it and you will produce empty pastes in plain-text editors.
For images, wrap the image blob in a ClipboardItem with the correct MIME type (image/png, image/jpeg, etc.). Mobile browsers and some desktop browsers restrict the set of image MIME types they accept; always check navigator.clipboard feature detection before showing the button.
Why "copy to clipboard" sometimes silently fails
The most common complaints we hear about copy buttons:
- "My copy button works on my machine but not in production." Staging or production is served over HTTP, not HTTPS. The Clipboard API is disabled on insecure origins.
- "It works on the first click but not when triggered from a callback." You lost the user gesture context. Move the
writeTextcall back into the click handler itself. - "It copies nothing / an empty string." Your source text is being read asynchronously and the promise resolves before
writeTextruns. Useawaitor callwriteTextwith the already-resolved value. - "It pastes with weird line breaks." You wrote
\non Windows where paste targets expect\r\n. Normalise line endings before writing, or write HTML instead of plain text.
For a deeper debugging workflow see How to Debug Paste Events in JavaScript and the Bulletproof Copy Button guide.
Legacy fallback: document.execCommand('copy')
Before navigator.clipboard, copying from JavaScript required a hack: place the text in a hidden <textarea>, select it, and call document.execCommand('copy'). This API is formally deprecated and removed from some recent browsers, but it still works in older Safari, in WebViews, and in environments where the async API is blocked. If you need to support both, feature-detect and fall back:
async function copyEverywhere(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text)
}
// Fallback for legacy / insecure contexts
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}For the full migration path see Migrating from execCommand to the Clipboard API.
Reading what you just copied
The fastest way to verify your copy button or your manual Ctrl+C is producing the data you expect: open Clipboard Inspector and paste. Every MIME type on the clipboard is shown with its decoded value. If you expected a text/html payload and only see text/plain, your ClipboardItem is missing a representation.