Add a 'copy code to clipboard' button to your blog's code blocks
Tags: webdev, javascript
Published: 2022-01-14 11:02:29

If you are writing about code, you are likely to include some code blocks to complement what it is you are discussing or explaining. To improve the experience for the reader, you can consider highlighting the syntax to make it easier to read, and adding a "copy code" button to make it simply for them to copy and paste the code (a key developer skill)! I will show you to do the latter.

## A simple code example

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>
```

In this example, I will use the syntax highlighting library [Prism](https://prismjs.com/) on the client-side. Prism is quite popular and is used in the static site generator [Eleventy](https://www.11ty.dev/docs/plugins/syntaxhighlight/), but there are [many libaries](https://www.dunebook.com/15-best-javascript-syntax-highlighting-libraries/) that do the same thing.

According to the [HTML5 spec](https://www.w3.org/TR/html52/textlevel-semantics.html#the-code-element), the recommended way to define the language for a code block is a through a `language-xxxx` class i.e. `language-css` for a CSS code block.

Prism looks for `code` elements that have `language-xxxx` classes 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 codepen of what I will cover below.{% codepen https://codepen.io/robjoeol/pen/poWQGLo %}

### CSS

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

Also, we want to add enough top padding to the `pre` to make space for the button, so that the text will never be covered by the button. On small screens this may happen.

```css
pre[class*="language-"] {
  position: relative;
  margin: 5px 0 ;
  padding: 1.75rem 0 1.75rem 1rem;

  /* more stuff */
}

pre[class*="language-"] button{
  position: absolute;
  top: 5px;
  right: 5px;

  /* more stuff */
}
```

You can place the button somewhere else, and style it differently if you prefer.

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

We only want to target the `pre` elements that contain a `code` element.

If you are using a static site generator that does the syntax highlighting at build time, and adds the class to the `pre`, you can use that to reference them.

If you don't have a class available, it is a bit tricker to target the code blocks since a query selection along the lines of "find me all pre elements that contain a code element" is not possible with `querySelectorAll()`, because currently there is no parent selector available in CSS syntax ([`has()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) is coming soon though). In that event, you would need to write an extra bit of JavaScript code to get a reference to these elements.

Writing to the system clipboard is quite straightforward. There is a browser API, the [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/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)](https://caniuse.com/async-clipboard). It is recommended that you use the Clipboard API instead of the deprecated [`document.execCommand()`](https://developer.mozilla.org/en-US/docs/Web/API/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 we call the async `writeText` function in it.

We can pass the `pre` block element as a parameter to get the text of the child `code` element. In the handler function, we can execute `querySelector("code")` on the `block` to get a reference to the child `code` element, and then get the text through the `innerText` property.

Let's put it all together then.

```javascript
const copyButtonLabel = "Copy Code";

// use a class selector if available
let blocks = document.querySelectorAll("pre");

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

**Warning: If you use a client-side syntax highlighting library and rely on it adding a class to the `pre`, keep in mind that you need to wait until that is finished first before you run the above code. It is preferable to avoid this scenario altogether.**

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

Prism adds an attribute to the `pre` to make it accessible, it adds `tabindex=0`. So, if you hit the <kbd>Tab</kbd> key, it will focus on the code block. You can then you use the right arrow key to scroll to the right.

<img src="https://www.roboleary.net/assets/img/blog/2022-01-13-copy-code-to-clipboard-blog/tabindex-attribute.png" width="493" height="728" alt="tabindex set on pre element by prism syntax highlighting library" loading="lazy">

**Not every syntax highlighting 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`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#tabindex) attribute to the code element, and give it a value of zero to make it focusable. 

	```javascript
	block.setAttribute("tabindex", 0);
	```

### Adding feedback for completion of task

The first thing that popped into my head was to change the button text to "Code Copied" when the action is done, and reset it to "Copy Code" after a small delay through `setTimeout()`. You can see a GIF of this in action below.

<img src="https://www.roboleary.net/assets/img/blog/2022-01-13-copy-code-to-clipboard-blog/feedback.gif" width="609" height="186" alt="feedback that code was copied to clipboard via changing of text of button to code copied" loading="lazy">

The JavaScript is short. Below I reset the text after 700 milliseconds. This time seems adequate to me.

```javascript
button.innerText = "Code Copied";

setTimeout(()=> {
  button.innerText = copyButtonLabel;
},700)
```

We add this code to our `copyCode` event handler, but we will also need to pass the `button` as a parameter.

```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` equal to the width of the button with the longer text (`min-width: 6.5rem;` will do the trick 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.