Building a Copy to Clipboard Button in JavaScript

Build a working copy-to-clipboard button with the Clipboard API. Covers navigator.clipboard.writeText, secure contexts, fallback with execCommand, and visual feedback.

5 min read

A copy-to-clipboard button lets users click once to copy text instead of selecting and pressing Ctrl+C. You see this pattern everywhere: code snippet "Copy" buttons, share-link copiers, and "Copy API key" fields.

The modern way to do it is with the Clipboard API, specifically navigator.clipboard.writeText().

The write is wrapped in try/catch so a failed copy does not break the page. setTimeout then restores the label after a moment so the button is ready for the next click:

javascriptjavascript
const copyButton = document.querySelector("#copy-btn");
const textToCopy = "Hello, clipboard!";
copyButton.addEventListener("click", async () => {
  try {
    await navigator.clipboard.writeText(textToCopy);
    copyButton.textContent = "Copied!";
  } catch (error) {
    copyButton.textContent = "Failed";
  }
  setTimeout(() => (copyButton.textContent = "Copy to Clipboard"), 2000);
});

Click the button and the text Hello, clipboard! is now in your system clipboard. The label switches to "Copied!" immediately, then resets two seconds later. That is the full pattern: write, confirm, reset.

Why writeText Works Only in Certain Conditions

The Clipboard API has two important requirements:

  1. Secure context: Your page must be served over HTTPS or localhost. HTTP pages do not have access to the clipboard for security reasons.

  2. User gesture: The write must happen inside a user-initiated event handler, like a click or keydown listener. You cannot write to the clipboard from setTimeout, setInterval, or on page load.

These restrictions prevent websites from silently overwriting your clipboard without your knowledge.

Copying Dynamic Content

In real use, the text to copy is usually not a hardcoded string. You read it from a nearby element, such as a code snippet sitting next to its own "Copy" button:

htmlhtml
<div class="code-block">
  <pre><code id="snippet">const x = 42;</code></pre>
  <button class="copy-btn">Copy</button>
</div>

Each button needs to find the code element in its own container, not a fixed one, so the same handler works for every copy button on the page. closest(".code-block") walks up to the shared wrapper, then querySelector("code") finds the sibling code element inside it:

javascriptjavascript
document.querySelectorAll(".copy-btn").forEach((button) => {
  button.addEventListener("click", async () => {
    const code = button.closest(".code-block").querySelector("code");
    await navigator.clipboard.writeText(code.textContent);
    button.textContent = "Copied!";
  });
});

This pattern works for any number of code blocks on the page because each button independently locates its own sibling code element. A production version should still wrap the write in try/catch and reset the label with setTimeout, following the same pattern shown earlier.

The Fallback: execCommand for Older Browsers

The navigator.clipboard.writeText API is supported in all modern browsers, so you rarely need a fallback. MDN marks document.execCommand as deprecated and non-standard, so only reach for it if you must support very old browsers that lack the Clipboard API:

javascriptjavascript
function fallbackCopy(text) {
  const textarea = document.createElement("textarea");
  textarea.value = text;
  textarea.style.position = "fixed"; // Prevent scrolling to it
  document.body.appendChild(textarea);
  textarea.select();
  document.execCommand("copy");
  document.body.removeChild(textarea);
}

This workaround creates an invisible &lt;textarea&gt;, fills it with the text, selects it, copies it, and removes the element. It is synchronous and blocking, but it works everywhere.

Combine both approaches for maximum compatibility:

javascriptjavascript
async function copyToClipboard(text) {
  if (navigator.clipboard && navigator.clipboard.writeText) {
    await navigator.clipboard.writeText(text);
  } else {
    fallbackCopy(text);
  }
}

Adding Good Visual Feedback

A button that silently copies text confuses users. Show success immediately, show failure clearly, and restore the original label after a short delay:

javascriptjavascript
copyButton.addEventListener("click", async () => {
  const originalHTML = copyButton.innerHTML;
  try {
    await navigator.clipboard.writeText(textToCopy);
    copyButton.innerHTML = "&#10003; Copied!";
  } catch {
    copyButton.textContent = "Copy failed";
  }
  setTimeout(() => (copyButton.innerHTML = originalHTML), 2000);
});

Saving originalHTML before the write means the reset works even if the button contains an icon plus text, not just plain text. Two seconds is enough for the confirmation to be noticeable without being distracting.

Copying from an Input Field

If the text is inside an &lt;input&gt; or &lt;textarea&gt;, you can read it directly:

javascriptjavascript
const input = document.querySelector("#share-link");
const copyBtn = document.querySelector("#copy-link-btn");
 
copyBtn.addEventListener("click", async () => {
  try {
    await navigator.clipboard.writeText(input.value);
    copyBtn.textContent = "Link copied!";
  } catch {
    copyBtn.textContent = "Copy failed";
  }
});

For input fields, you could also use input.select() followed by document.execCommand("copy") as an alternative to the Clipboard API, but writeText is simpler and more modern.

Common Mistakes

Calling writeText outside a user gesture. Clipboard writes must happen synchronously within a user-initiated event. Wrapping them in setTimeout or calling them from a fetch response handler fails with a NotAllowedError.

This is why the clipboard logic lives inside a click listener. See /javascript/how-to-add-event-listeners-in-js-complete-guide for the fundamentals of setting up event handlers.

Forgetting to handle errors. Clipboard access can fail for reasons beyond your control: the user denied permission, the browser does not support it, or the page is served over HTTP. Always use try/catch.

Not testing on HTTP during development. If you test on localhost, the Clipboard API works fine. When you deploy to an HTTP server, it silently fails. Test your deployed environment, or redirect HTTP to HTTPS.

For more event-driven UI patterns, see /javascript/handling-click-events-in-javascript-full-guide.

Rune AI

Rune AI

Key Insights

  • Use navigator.clipboard.writeText(text) to copy text to the clipboard.
  • The Clipboard API requires HTTPS or localhost and a user gesture like a click.
  • Show a confirmation message after copying, then reset it after a short delay.
  • Use a temporary textarea with execCommand('copy') as a fallback for older browsers.
  • Always handle errors; clipboard access can fail for security reasons.
RunePowered by Rune AI

Frequently Asked Questions

Why does navigator.clipboard.writeText not work on my site?

The Clipboard API requires a secure context, meaning your page must be served over HTTPS or from localhost. It also requires user interaction; you cannot write to the clipboard without a click, keypress, or similar user gesture.

Do I need to ask the user for permission to write to the clipboard?

Usually not. In most browsers, calling writeText inside a user-initiated event handler, like a click event, avoids a visible prompt. The exact rule varies by browser: some grant clipboard-write automatically for a gesture-triggered write, others cache the permission after the first grant. Always test in your target browsers.

What is the fallback for older browsers that do not support the Clipboard API?

Use document.execCommand('copy') as a fallback. Select the text with a temporary textarea element, call execCommand, then remove the textarea. This approach works in all browsers but is synchronous and blocking.

Conclusion

A copy-to-clipboard button is a small but useful feature built with the Clipboard API. Use navigator.clipboard.writeText() as the primary method, fall back to execCommand('copy') for older browsers, and always show clear visual feedback so the user knows the copy succeeded.