Adding Copy Buttons to Code Blocks
[ web development · javascript · astro · ux ]
A simple but essential user experience improvement: one-click copying for code snippets.
The Problem
When readers encounter code examples, they often want to copy them for testing or reference. Without a copy button, users must manually select all the text, which is a tedious process that’s especially frustrating on mobile devices or with longer code blocks.
The Solution
I implemented copy buttons that appear alongside the existing language badges, using the Clipboard API for reliable copying and providing visual feedback when the operation succeeds.
document.addEventListener("DOMContentLoaded", function () {
const codeBlocks = document.querySelectorAll("pre[data-language]");
codeBlocks.forEach(function (pre) {
const copyButton = document.createElement("button");
copyButton.className = "copy-button";
copyButton.innerHTML = '<svg>...</svg>'; // Clipboard icon
copyButton.setAttribute("aria-label", "Copy code to clipboard");
copyButton.addEventListener("click", function () {
const code = pre.querySelector("code");
if (!code) return;
const text = code.textContent || code.innerText;
navigator.clipboard.writeText(text).then(function () {
// Show checkmark feedback
copyButton.innerHTML = '<svg>...</svg>'; // Check icon
setTimeout(function () {
copyButton.innerHTML = '<svg>...</svg>'; // Back to clipboard
}, 2000);
});
});
pre.appendChild(copyButton);
});
});
Implementation Details
The approach builds on the existing code block infrastructure:
- DOM injection: JavaScript dynamically adds copy buttons to each
pre[data-language]
element - Modern API usage: Uses
navigator.clipboard.writeText()
for secure, reliable copying - Visual feedback: Button icon changes to a checkmark for 2 seconds after successful copy
- Accessibility: Includes proper
aria-label
for screen readers
Styling Integration
The copy button is positioned to complement the existing language badge:
/* Copy button positioned on the far right */
.copy-button {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
border: none;
padding: 3px 4px;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
transition: background 0.2s ease;
}
/* Language badge positioned to the left of copy button */
pre[data-language]::before {
content: attr(data-language);
position: absolute;
bottom: 8px;
right: 36px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7rem;
z-index: 1;
}
Key design decisions:
- Consistent positioning: Matches the language badge’s bottom alignment and styling
- Visual hierarchy: Copy button sits on the far right for easy access, with language badge to its left
- Simple spacing: Fixed 28px gap between elements eliminates complexity of dynamic positioning
- Theme integration: Inherits the same dark/light mode behavior as other interface elements
- Hover states: Subtle background changes provide interaction feedback
Why This Matters
Copy buttons remove friction from the code sharing experience. They’re especially valuable for:
- Mobile users who struggle with text selection
- Complex code blocks where manual selection is error-prone
- Accessibility since they provide an alternative to drag-selection
This enhancement demonstrates how small UX improvements can significantly impact user experience. The implementation is lightweight, progressively enhanced, and integrates seamlessly with the existing design system.