Appearance
Clipboard Copy with Universal Fallback
navigator.clipboard.writeText() only exists in a secure context. On plain http://, on file://, or when a permission is denied, it is absent or rejects. A copy feature that dead-ends at a "clipboard unavailable" message fails exactly where a self-contained artifact is most likely to run: opened from the file system or served over plain HTTP. Degrade through three tiers so a copy always succeeds, or at worst leaves the text selected for a manual copy.
When to use
Use this pattern whenever code copies text to the clipboard and cannot assume a secure context: single-file HTML artifacts, prototypes opened from disk, tools served over plain HTTP, or anything a human might run locally.
The pattern
Try each tier in order and stop at the first that works:
- async
navigator.clipboard.writeText()(secure context); - synchronous
document.execCommand("copy")through a throwaway off-screen<textarea>(covers plainhttp://andfile://); - a small dialog with the text pre-selected and a Copy button (plus a "press ⌘C/Ctrl+C" hint), so there is always a manual path.
javascript
async function copy(text, okMessage) {
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(text);
toast(okMessage);
return;
} catch (error) {
// Secure-context API rejected (denied permission): drop a tier.
}
}
// No secure-context API, or it rejected (plain http, file://): drop a tier.
if (execCopy(text)) toast(okMessage);
else showCopyBox(text);
}Tier 2: synchronous execCommand
execCommand("copy") is deprecated but remains the only synchronous copy path on non-secure origins. Select the text in an off-screen <textarea>, copy, then remove it:
javascript
function execCopy(text) {
const textarea = document.createElement("textarea");
textarea.value = text;
// Park it off-screen rather than styling in JS; a class keeps CSS in CSS.
textarea.className = "copy-source";
textarea.setAttribute("readonly", "");
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, text.length);
let ok = false;
try {
ok = document.execCommand("copy");
} catch (error) {}
textarea.remove();
return ok;
}css
.copy-source {
position: fixed;
top: -1000px;
opacity: 0;
}Tier 3: manual copy dialog
When even execCommand is blocked, show the text already selected with a Copy button that retries execCommand, falling back to a keyboard hint:
javascript
function showCopyBox(text) {
const box = document.querySelector(".copy-box");
const textarea = box.querySelector(".copy-text");
textarea.value = text;
box.showPopover();
textarea.focus();
textarea.select();
}Detect the platform so the hint names the right key:
javascript
const isMac = /Mac|iP(hone|ad|od)/.test(
navigator.platform || navigator.userAgent || "",
);
const copyKey = isMac ? "⌘C" : "Ctrl+C";Trade-offs
execCommandis deprecated but irreplaceable on non-secure origins, where the async Clipboard API does not exist. Keep it as the middle tier rather than removing it.- Tier 3 always succeeds at something. Even if no programmatic copy works, the text is selected and ready, so the human can copy it by hand. The feature never dead-ends.
See also
- Popover API for Overlays: Render the fallback dialog and the confirmation toast in the top layer without z-index juggling.