How to add a copy to clipboard button to code blocks
If you are writing about code, you are likely to include some code blocks to complement what it is you are discussing. To improve the experience for the reader, you can consider adding a “copy code to clipboard” button to make it simple to copy and paste the code - a key developer skill after all!
In this article, I will show you how to: use the Clipboard API to asynchronously write to the system clipboard, position and style the button in a code block, and provide feedback to the user that the code was actually copied!
A blog example with a copy button on every code block
We only want to add our button to code blocks that are wrapped inside a pre such as below:
<pre><code class="language-css">.some-box {
width: 20px;
height: 20px;
background: black;
margin-bottom: 1.5rem;
}
</code></pre> According to the HTML5 spec, the recommended way to define the language for a code block is a through adding a class in the form of language-xxxx where xxxx is the language name i.e. a class of language-css for a CSS code block as per example above.
I have styled the code blocks with a syntax highlighting library to create a familiar looking style for all of the code blocks. It is preferable to do this on the server-side. If you use a syntax highlighter library on the client side, it adds elements to the page which has the potential to cause a layout shift. This degrades user experience and is best avoided.
Here I have used the Shiki syntax highlighter to produce the markup for the code blocks that I have included in the HTML file. Shiki adds the styles inline i.e. using the style attribute for each element in the code block.
Here is the demo that I will cover. The copy button is included in the markup on the server side. This is the preferred approach. Adding the button on the client side can led to a layout shift that can be challenging to prevent.
If you want to add the copy button on the client side. You can explore this example.
The CSS code
You probably want to position the button in one of the top corners of the pre. You need to handle when the code overflows the pre block. You can do one of the following:
- Top-left corner: Set
position: stickyon the button, and use thetopandleftproperties to position the button in the corner of the block. - Top-right corner: The gotcha with the top-right corner is that when the code overflows, the
buttonwill be sitting out of view in the overflowed portion of the element’s content. This can be overcome by adding a sticky header to theprethat contains the button.
Position the button in top-left corner
<pre><button>Copy Code</button><code class="language-javascript">console.log("a long text string that overflows");</code>
</pre>/* copy button */
pre:has(code) button {
display:block;
position: sticky;
top: 0;
left: 0;
margin-block: 0.25rem 0.5rem;
}
/* code block */
pre:has(code) {
position: relative;
/* add scrollbar if there is overflow */
overflow-y: auto;
/* trigger overflow for purpose of demo */
width: 200px;
background-color:black;
color:white;
padding-inline: 0.25rem;
padding-block-end: 1rem;
}Result
I am targetting pre blocks that contain a code element using the :has() pseudo-class. To position everything, we set the pre code blocks as position: relative to ensure that our button will be positioned relative to it, and not the body. The button is then set as position: sticky and positioned to the top left of the pre.
Position the button in top-right corner
The major difference in this example is that we have a header in the pre block that contains our copy button.
<pre><header><button>Copy Code</button></header><code class="language-javascript">console.log("a long text string that overflows");</code>
</pre>/* header sticks to top of pre code blocks */
pre header {
position: sticky;
top: 0;
left: 0;
width: 100%;
/* our button is placed at right side/end of header */
display: flex;
justify-content: end;
}
/* code block */
pre:has(code) {
position: relative;
/* add scrollbar if there is overflow */
overflow-y: auto;
/* trigger overflow for purpose of demo */
width: 200px;
background-color:black;
color:white;
padding-inline: 0.25rem;
padding-block-end: 1rem;
}
/* copy button */
pre:has(code) button{
margin-block: 0.25rem 0.5rem;
}Result
I am targetting pre blocks that contain a code element using the :has() pseudo-class. To position everything, we set the pre code blocks as position: relative to ensure that our header will be positioned relative to it, and not the body. The header is then set as position: sticky and positioned to the top left of the pre. It is given width: 100% to span the entire width of the pre. Now if there is overflow, the header will always be on top as the user scrolls across. We position our button by making our header a flexbox container and we use justify-content: end to move our button to the right-hand side for left-to-right languages, and the opposite side for right-to-left languages.
We cater for horizontal overflow with overflow-y: auto;, which adds a scrollbar if needed.
That’s the important layout styling. The rest is up to you with regard to how it looks. Some people like to hide the button and only show it when you hover over the code block. I think that is not good for user experience. You shouldn’t need to uncover this type of functionality!
The JavaScript code
Writing to the system clipboard is quite straightforward. There is a browser API, the Clipboard API, which enables you to asynchronously read from and write to the system clipboard. The browser support is excellent (for writing to the clipboard). It is recommended that you use the Clipboard API instead of the deprecated document.execCommand() method.
To access the clipboard, you use the navigator.clipboard global. To write to the clipboard there is an async writeText() function.
await navigator.clipboard.writeText("some text"); In the event handler for the button, we want to get the text of the code element and copy it to the clipbboard. We will name our event handler copyCode and make it async because of the call to the async writeText function.
We can pass the pre block element as a parameter. In the handler function, we get a reference to the child code element, and then we get its text through its innerText property.
Putting this all together, the code looks like this:
let blocks = document.querySelectorAll("pre:has(code)");
blocks.forEach((block) => {
let button = block.querySelector("button");
// handle click event
button.addEventListener("click", async () => {
await copyCode(block);
});
});
async function copyCode(block) {
let code = block.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
} Before we call it a day, we should check if our code blocks are accessible to keyboard users. And also, it would be nice to give some visual feedback to indicate to the user that the task was completed successfully.
Accessibility
One thing that is often overlooked is accessibility! Code blocks are not focusable by a keyboard by default. This means that if your audience is using only a keyboard to navigate the site, they will be unable to access the content that has overflowed the container. We want the code blocks to be a tab stop to enable users to select a code block.
Some syntax highlighter libraries such as Prism add the attribute-value pair of tabindex=0 to the pre to make it a tab stop. If you hit the Tab key, it will focus on the code block. You can then you use the right arrow key to scroll to the right. Not every syntax highligthing library will make a code block a tab stop for you, so check to be sure!
If this is not done for you, you have 2 options to fix it:
-
In CSS, you could prevent overflow from happening by forcing text to break onto the next line. The following rule will achieve that.
CSS code[class*="language-"] { white-space: pre-wrap; word-break: break-all; } -
In JavaScript, you can add
tabindexattribute to thepreelement, and give it a value of zero to make it focusable.Javascript block.setAttribute("tabindex", 0);
Adding feedback for completion of task
The simplest way to provide feedback to the user that the copy action is done is to change the text of the button. When the action is done, we can change it to “Copied”, and then we can reset it to its initial value after a small delay. You can see a this in action below.
The JavaScript is short. Below I reset the text after 700 milliseconds through setTimeout(). This time seems adequate to me. The duration is arbitary, you can choose a different duration if you like!
let copyButtonLabel = "Copy Code";
button.innerText = "Code Copied";
setTimeout(()=> {
button.innerText = copyButtonLabel;
},700) We add this code to our copyCode event handler. Also, we need to add the button as a parameter to be able to change its text.
let copyButtonLabel = "Copy Code";
async function copyCode(block, button) {
let code = block.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
// visual feedback that task is completed
button.innerText = "Code Copied";
setTimeout(() => {
button.innerText = copyButtonLabel;
}, 700);
} If you don’t like the fact that the button grows in size when the text is switched, you can set a min-width on the button. You can set the min-width to the width of the button when it has the text “Code Copied”. This would be min-width: 6.5rem; in my case.
If you want to provide feedback in another way, you could show a toast notification or create an animation of some sort.