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.
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:
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:
-
Secure context: Your page must be served over HTTPS or
localhost. HTTP pages do not have access to the clipboard for security reasons. -
User gesture: The write must happen inside a user-initiated event handler, like a
clickorkeydownlistener. You cannot write to the clipboard fromsetTimeout,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:
<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:
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:
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 <textarea>, 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:
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:
copyButton.addEventListener("click", async () => {
const originalHTML = copyButton.innerHTML;
try {
await navigator.clipboard.writeText(textToCopy);
copyButton.innerHTML = "✓ 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 <input> or <textarea>, you can read it directly:
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
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.
Frequently Asked Questions
Why does navigator.clipboard.writeText not work on my site?
Do I need to ask the user for permission to write to the clipboard?
What is the fallback for older browsers that do not support the Clipboard API?
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.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.