Copy text to the clipboard
The modern clipboard write with the execCommand fallback older embeds still need — returns whether the copy actually happened.
Last updated
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Older browsers, or a non-secure context: fall back to execCommand.
const el = document.createElement("textarea");
el.value = text;
el.setAttribute("readonly", "");
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
const ok = document.execCommand("copy");
el.remove();
return ok;
}
}How it works
navigator.clipboard.writeText is the modern path — asynchronous, permission-aware, and the only one that works inside newer sandboxed contexts. The try/catch treats every possible refusal (no permission, insecure origin, an old engine) identically: fall back rather than interrogate.
The fallback creates an invisible readonly textarea, selects it, and uses the deprecated-but-everywhere execCommand. position:fixed stops the page from scrolling to the element on focus, which is the classic visible glitch in naive versions.
Edge cases to know
- →The clipboard API requires a secure context (https or localhost) — on plain http you will always be on the fallback path.
- →Both paths must run inside a user gesture (a click handler); calling on page load fails silently in most browsers.
- →execCommand is formally deprecated: keep it as the fallback, never the primary, and expect it to disappear from engines eventually.
Part by part
- navigator.clipboard.writeText(text)
- The modern API: asynchronous, permission-aware, and the only path that works in newer sandboxed contexts. It requires a secure origin.
- catch {
- A bare catch, treating every refusal identically — no permission, insecure origin, ancient engine. Interrogating which one it was leads nowhere useful; falling back handles all of them.
- el.setAttribute("readonly", "")
- Stops mobile keyboards appearing when the hidden textarea is focused, which is the difference between an invisible fallback and a visible flicker.
- el.style.position = "fixed"
- Keeps the element out of document flow so the page does not scroll to it when selected — the most visible symptom of a naive fallback.
- document.execCommand("copy")
- The deprecated path, kept because it works without a secure origin and in older browsers. It is synchronous, which is why the fallback needs no await.
Common mistakes
await navigator.clipboard.writeText(text)On its own it throws on an insecure origin — which includes any page served over plain HTTP, and some embedded contexts. Without a fallback the copy button simply does nothing on those pages.
Calling it outside a user gesture.Browsers require a click or keypress for clipboard writes, so copying on page load or after a timer is rejected regardless of which API you use. The rejection looks identical to a permissions problem.
Omitting the position and opacity styles on the fallback.The textarea is briefly rendered and selected, so the page jumps to it and a mobile keyboard appears. It works, and it looks broken.
Questions
Why does it fail on my local network?
The Clipboard API needs a secure context, which means HTTPS or localhost. A dev server reached at a LAN address is neither, so the modern path throws and the fallback is what actually runs.
Is execCommand safe to keep using?
It is deprecated and still widely supported, which is exactly why it makes a good fallback rather than a primary. Expect to remove it eventually, and not soon.
How do I tell the user it worked?
The function returns a boolean, so show a brief confirmation on true and an error on false. A copy button with no feedback is the most common complaint about this pattern.
Can I copy rich content or images?
Yes, with navigator.clipboard.write and a ClipboardItem, which supports HTML and images. Support is narrower than writeText, so feature-detect rather than assuming.