How to add a copy to clipboard button to code blocks

A copy to clipboard icon is set against a light purple background. The icon is a white rectangle with a second cropped rectangle behind it in an offset position.

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, how you can position and style the button in a code block, and how you can 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:

HTML
<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.

As an aside, I will highlight the code with a syntax highlighting library to create a familiar looking style for all of the code blocks, just like you would find in your code editor. In this example, I will use the popular Prism library. Prism looks for all code elements that have a language-xxxx class to identify blocks to add highlighting to. Prism copies the language-xxxx class from the code element to the pre for you. This will make it easier for you to target the correct elements in your CSS later.

Here is the demo that I will cover.

CSS code

I want to position the button in the top right corner of the pre. To achieve this placement, we set the pre as position: relative and the button as position: absolute, and set the top and right properties of the button.

I am targetting pre blocks that contain a code element using the :has() pseudo-class i.e. pre:has(code).

CSS
pre:has(code) {
position: relative;
margin: 5px 0 ;
padding: 1.75rem 0 1.75rem 1rem;

/* more stuff */
}

pre:has(code) button{
position: absolute;
top: 5px;
right: 5px;

/* more stuff */
}

I add padding to the start of the pre block to make space for the button to ensure that the text will never be covered by the button. On small screens this is going to happen otherwise.

You can style things differently of course! You may want to place the button somewhere else. Some people like to hide the button and only show it when you hover over the code block. I think that is bad for user experience. You shouldn’t need to uncover this type of functionality!

JavaScript code

Again, we only want to target the pre elements that contain a code element. We can provide the pre:has(code) selector to the querySelectorAll() function to retrieve all elements that fit this criterion.

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.

Javascript
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 to get the text of the child code element. In the handler function, we get a reference to the child code element by let code = block.querySelector("code"), and then we get its text through its innerText property.

Putting this all together, the code looks like this:

Javascript
let blocks = document.querySelectorAll("pre:has(code)");
let copyButtonLabel = "Copy Code";


blocks.forEach((block) => {
// only add button if browser supports Clipboard API
if (navigator.clipboard) {
let button = document.createElement("button");

button.innerText = copyButtonLabel;
block.appendChild(button);

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);
}

While the above code works perfectly well, we should check if it is accessible to keyboard user. 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, which would usually be scrolled with a mouse.

Prism adds an attribute to the pre to make it make a tab stop - it adds the attribute-value pair of tabindex=0. So, 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.

tabindex set on pre element by prism syntax highlighting library

Not every syntax highligthing library will do this for you, so check to be sure!

If this is not done for you, you have 2 options to fix it:

  1. 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;
    }
  2. In JavaScript, you can add tabindex attribute to the pre element, 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 GIF of this in action below.

feedback that code was copied to clipboard via changing of text of button to code copied

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!

Javascript
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.

Javascript
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.

Tagged