Skip to content
WANCODE
BM

Copy-to-Clipboard Helper

Copy text with the async Clipboard API, fall back gracefully, and tell the user (and screen readers) whether it worked.

Live preview Open in Playground
copy-text.js
/**
 * 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.isSecureContext is true on HTTPS and localhost. 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 runs document.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/false instead 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

HTML
<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>