Copy-to-Clipboard Helper
Copy text with the async Clipboard API, fall back gracefully, and tell the user (and screen readers) whether it worked.
/**
* Copies text to the clipboard. Resolves to true on success.
* Must be called from a user gesture (click, key press).
*/
export async function copyText(text) {
// Modern API: needs a secure context (HTTPS or localhost)
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Permission denied or blocked (e.g. inside some iframes) — try the fallback
}
}
return legacyCopy(text);
}
function legacyCopy(text) {
const previousFocus = document.activeElement;
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", ""); // no on-screen keyboard on phones
textarea.style.position = "fixed"; // don't scroll the page
textarea.style.opacity = "0";
document.body.append(textarea);
textarea.select();
let ok = false;
try {
ok = document.execCommand("copy"); // deprecated, but still the only fallback
} catch {
ok = false;
}
textarea.remove();
if (previousFocus instanceof HTMLElement) previousFocus.focus();
return ok;
}navigator.clipboard.writeText() is the modern way to copy. It returns a promise because the browser may need to check permissions first. It can fail — on plain http:// pages, without a user gesture, or inside an iframe that hasn't been granted clipboard access — so we always handle failure.
Line by line
window.isSecureContextistrueon HTTPS andlocalhost. The Clipboard API simply doesn't exist (or is blocked) elsewhere.try { await … } catch { … }turns a rejected promise into a normal code path instead of an "Uncaught (in promise)" error.- The fallback puts the text in an invisible
<textarea>, selects it and runsdocument.execCommand("copy"). That API is deprecated, but browsers still support it and it's the only option when the modern one is blocked. - Selecting the textarea steals focus, so we put focus back where it was. Keyboard users stay on the button they pressed.
- The function returns
true/falseinstead of showing UI itself, so you decide how to report the result.
Announce the result in an element with role="status" (a polite live region) so screen-reader users hear "Copied" too. Always offer a manual way (visible, selectable text) in case copying fails.
In the preview
The WANCode preview is a sandboxed iframe without clipboard permission, so the modern API is usually blocked there and the fallback (or the failure message) is what you'll see. That's a realistic test of the error path.
Usage
<code id="cmd">npm install</code>
<button type="button" id="copy">Copy</button>
<span role="status" id="copy-status"></span>
<script type="module">
import { copyText } from "./copy-text.js";
const status = document.getElementById("copy-status");
document.getElementById("copy").addEventListener("click", async () => {
const ok = await copyText(document.getElementById("cmd").textContent);
status.textContent = ok ? "Copied!" : "Couldn't copy — select the text and press Ctrl+C.";
});
</script>