Building a Copy to Clipboard Button in JavaScript
A complete tutorial on building a copy-to-clipboard button in JavaScript. Covers navigator.clipboard.writeText, fallback with document.execCommand, visual feedback with success and error states, copying from code blocks and input fields, accessibility with ARIA attributes, tooltip-based feedback, framework-agnostic patterns, and building a reusable CopyButton component.
A copy-to-clipboard button is one of the most common UI patterns in documentation sites, code playgrounds, and dashboards. This guide walks through building a production-ready implementation with the modern Clipboard API, legacy fallbacks, visual feedback, and accessibility.
For the full Clipboard API reference, see JavaScript Clipboard API: a complete tutorial.
Minimal Implementation
const button = document.getElementById("copy-btn");
const target = document.getElementById("code-block");
button.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(target.textContent);
button.textContent = "Copied!";
setTimeout(() => (button.textContent = "Copy"), 2000);
} catch (error) {
console.error("Copy failed:", error.message);
button.textContent = "Failed";
setTimeout(() => (button.textContent = "Copy"), 2000);
}
});Fallback for Older Browsers
navigator.clipboard requires HTTPS and a secure context. For HTTP pages or older browsers, fall back to document.execCommand:
function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
// Legacy fallback
return new Promise((resolve, reject) => {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.cssText =
"position:fixed;top:-9999px;left:-9999px;opacity:0";
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand("copy");
document.body.removeChild(textarea);
success ? resolve() : reject(new Error("execCommand copy failed"));
} catch (error) {
document.body.removeChild(textarea);
reject(error);
}
});
}
// Usage
await copyToClipboard("Hello, world!");Copy Button With Visual Feedback
class CopyButton {
constructor(element, options = {}) {
this.button = element;
this.targetSelector = element.dataset.copyTarget;
this.staticText = element.dataset.copyText;
this.originalHTML = element.innerHTML;
this.feedbackDuration = options.feedbackDuration || 2000;
this.successHTML = options.successHTML || "Copied!";
this.errorHTML = options.errorHTML || "Failed";
this.timer = null;
this.isCopying = false;
this.button.addEventListener("click", () => this.copy());
}
async copy() {
if (this.isCopying) return;
this.isCopying = true;
const text = this.getText();
if (!text) {
this.showFeedback(this.errorHTML, "error");
return;
}
try {
await copyToClipboard(text);
this.showFeedback(this.successHTML, "success");
} catch {
this.showFeedback(this.errorHTML, "error");
}
}
getText() {
if (this.staticText) return this.staticText;
if (this.targetSelector) {
const target = document.querySelector(this.targetSelector);
if (!target) return null;
// For code blocks, get textContent to strip HTML
if (target.tagName === "CODE" || target.tagName === "PRE") {
return target.textContent;
}
// For inputs, get value
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
return target.value;
}
return target.textContent;
}
return null;
}
showFeedback(html, state) {
clearTimeout(this.timer);
this.button.innerHTML = html;
this.button.dataset.state = state;
this.button.setAttribute("aria-label", state === "success" ? "Copied" : "Copy failed");
this.timer = setTimeout(() => {
this.button.innerHTML = this.originalHTML;
delete this.button.dataset.state;
this.button.setAttribute("aria-label", "Copy to clipboard");
this.isCopying = false;
}, this.feedbackDuration);
}
}HTML Structure
<div class="code-block-wrapper">
<pre><code id="example-code">const greeting = "Hello, world!";
console.log(greeting);</code></pre>
<button
class="copy-btn"
data-copy-target="#example-code"
aria-label="Copy to clipboard"
>
Copy
</button>
</div>CSS for Copy Button States
// Add this CSS dynamically or in your stylesheet
const styles = `
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
padding: 4px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #f9fafb;
color: #374151;
font-size: 13px;
cursor: pointer;
transition: all 0.15s ease;
}
.copy-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.copy-btn[data-state="success"] {
background: #dcfce7;
border-color: #86efac;
color: #166534;
}
.copy-btn[data-state="error"] {
background: #fef2f2;
border-color: #fca5a5;
color: #991b1b;
}
.code-block-wrapper {
position: relative;
}
`;
const styleEl = document.createElement("style");
styleEl.textContent = styles;
document.head.appendChild(styleEl);SVG Icon Button With Tooltip
class IconCopyButton {
constructor(element) {
this.button = element;
this.timer = null;
this.copyIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>`;
this.checkIcon = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>`;
this.button.innerHTML = this.copyIcon;
this.button.setAttribute("aria-label", "Copy to clipboard");
this.button.title = "Copy";
this.button.addEventListener("click", () => this.copy());
}
async copy() {
const target = document.querySelector(this.button.dataset.copyTarget);
if (!target) return;
try {
await copyToClipboard(target.textContent);
this.showSuccess();
} catch {
this.button.title = "Copy failed";
setTimeout(() => (this.button.title = "Copy"), 2000);
}
}
showSuccess() {
clearTimeout(this.timer);
this.button.innerHTML = this.checkIcon;
this.button.title = "Copied!";
this.button.classList.add("copied");
this.timer = setTimeout(() => {
this.button.innerHTML = this.copyIcon;
this.button.title = "Copy";
this.button.classList.remove("copied");
}, 2000);
}
}Auto-Initializing All Code Blocks
function initCopyButtons(options = {}) {
const selector = options.codeSelector || "pre > code";
const codeBlocks = document.querySelectorAll(selector);
codeBlocks.forEach((code, index) => {
const pre = code.closest("pre");
if (!pre) return;
// Skip if already has a copy button
if (pre.parentElement.querySelector(".copy-btn")) return;
// Wrap in a relative container
const wrapper = document.createElement("div");
wrapper.className = "code-block-wrapper";
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
// Create copy button
const button = document.createElement("button");
button.className = "copy-btn";
button.setAttribute("aria-label", "Copy to clipboard");
code.id = code.id || `code-block-${index}`;
button.dataset.copyTarget = `#${code.id}`;
wrapper.appendChild(button);
new IconCopyButton(button);
});
}
// Initialize on page load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => initCopyButtons());
} else {
initCopyButtons();
}Accessibility Considerations
| Requirement | Implementation |
|---|---|
| Keyboard accessible | Use <button> element (focusable by default) |
| Screen reader label | aria-label="Copy to clipboard" |
| Status announcement | aria-live="polite" region for feedback |
| Visual focus indicator | CSS :focus-visible outline |
| State communication | Update aria-label on success/error |
// Accessible announcement
function announceToScreenReader(message) {
let region = document.getElementById("copy-announcer");
if (!region) {
region = document.createElement("div");
region.id = "copy-announcer";
region.setAttribute("aria-live", "polite");
region.setAttribute("aria-atomic", "true");
region.className = "sr-only"; // visually hidden
document.body.appendChild(region);
}
region.textContent = "";
// Small delay to trigger announcement on repeated copies
requestAnimationFrame(() => {
region.textContent = message;
});
}
// In your copy handler:
try {
await copyToClipboard(text);
announceToScreenReader("Code copied to clipboard");
} catch {
announceToScreenReader("Failed to copy code");
}Copying Different Content Types
| Source | Extraction Method | Notes |
|---|---|---|
<pre><code> | element.textContent | Strips syntax highlighting spans |
<input> | element.value | Works for text, email, URL inputs |
<textarea> | element.value | Preserves newlines |
<td> / <th> | element.textContent | Single cell content |
<table> | Custom formatter | Tab-separated for spreadsheet paste |
<div contenteditable> | element.innerText | Preserves visual line breaks |
// Copy an entire table as tab-separated values
function copyTable(tableElement) {
const rows = tableElement.querySelectorAll("tr");
const lines = [];
rows.forEach((row) => {
const cells = row.querySelectorAll("td, th");
const values = [...cells].map((c) => c.textContent.trim());
lines.push(values.join("\t"));
});
return copyToClipboard(lines.join("\n"));
}Rune AI
Key Insights
- Always provide a fallback: The Clipboard API requires HTTPS; use
document.execCommand('copy')with a hidden textarea for HTTP and older browsers - Visual feedback is essential: Users need confirmation that the copy succeeded; swap the button text or icon for 2 seconds then reset
- Use textContent for code blocks:
textContentstrips HTML tags from syntax-highlighted code, giving users clean source code - Accessibility matters: Use semantic
<button>elements, announce copy results witharia-liveregions, and updatearia-labelon state changes - Auto-initialize on all code blocks: Wrap each
<pre>in a relative container and inject copy buttons programmatically for consistent documentation UX
Frequently Asked Questions
Why does copy fail on HTTP pages?
Can I copy without a user click event?
How do I copy formatted code with syntax highlighting?
Should I use a library or build my own?
How do I test copy-to-clipboard in automated tests?
Conclusion
A well-built copy button needs the modern Clipboard API, an execCommand fallback, clear visual feedback, and proper accessibility. Auto-initialize buttons on all code blocks for documentation sites, use icon buttons with tooltips for compact UIs, and always announce state changes to screen readers. For the underlying Clipboard API details, see JavaScript Clipboard API: a complete tutorial. For storing copied content, see JS localStorage API guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.