How to add GitHub Copy to Clipboard button on your docs/blog
If you usually use GitHub Markdown, you may have seen the small copy
button at the top right corner of code blocks:
This is specially useful for documentation pages (like the majority of the content in this website), as it allow your users to easily paste your documentation code in their project. I wanted to add the copy
button to my open source docs since I first saw that little thing on GitHub, and yesterday I finally figured it out.
Background
All the syntax highlighting in my docs are powered by Spatie's laravel-markdown, which uses shiki-php under the hood. Also, I'm using Tailwind CSS to handle all styling related tasks.
When highlighting code, shiki
adds all the markdown inside a pre
tag with a shiki
class. For example, the following markdown code:
```php
use Junges\Kafka\Facades\Kafka;
Kafka::publishOn('topic')
will be converted to this HTML:
<pre class="shiki" style="background-color: #ffffff">
<code>
<span class="line">
<span style="color: #CF222E">use</span>
<span style="color: #24292F"> </span>
<span style="color: #0550AE">Junges\Kafka\Facades\Kafka</span>
<span style="color: #24292F">;</span>
</span>
<span class="line"></span>
<span class="line">
<span style="color: #0550AE">Kafka</span>
<span style="color: #CF222E">::</span>
<span style="color: #8250DF">publishOn</span>
<span style="color: #24292F">(</span>
<span style="color: #0A3069">'topic'</span>
<span style="color: #24292F">)</span>
</span>
<span class="line"></span>
</code>
<button class="button-copy-code">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="copy-docs-icon">
<path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
<path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path>
</svg>
</button>
</pre>
We want to add our copy
button only on code
tags that are wrapped inside a pre
such as above, and this will be the final result:
Creating the copy
button with Javascript
The first thing we need to do is select all HTML pre
element presents in the page:
let blocks = document.querySelectorAll("pre");
With all blocks of pre
elements selected, we can proceed adding one new button
element to each of them, but we will only do that if the clipboard
API is available, to avoid user experience issues:
blocks.forEach((block) => {
if (! navigator.clipboard) {
return;
}
let button = document.createElement("button");
button.className = "button-copy-code";
button.innerHTML = copyIcon;
block.appendChild(button);
I also added the button-copy-code
HTML class to the button, to make it easier to style it and select parent elments in the next steps.
The next step is to add a listener for the click
event of our button
, which will be responsible for copying the code within the markdown:
button.addEventListener("click", async () => {
await copyCode(block);
});
});
I named the event handler copyCode
, and it's async
because of the async call to the clipboard
API writeText
function.
The copyCode
function looks like this:
async function copyCode(block) {
let copiedCode = block.cloneNode(true);
copiedCode.removeChild(copiedCode.querySelector("button.button-copy-code"));
const html = copiedCode.outerHTML.replace(/<[^>]*>?/gm, "");
block.querySelector("button.button-copy-code").innerHTML = copiedIcon;
setTimeout(function () {
block.querySelector("button.button-copy-code").innerHTML = copyIcon;
}, 2000);
const parsedHTML = htmlDecode(html);
await navigator.clipboard.writeText(parsedHTML);
}
In this function we need to strip all HTML out of the string before adding it to the clipboard. This is done by the copiedCode.outerHTML.replace(/<[^>]*>?/gm, "")
call, that gives us a string with only the content within the pre
tag.
Also, we need decode the html
returned using another function, to correctly copy the content such as <
, >
, and so on.
function htmlDecode(input) {
const doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
Putting this all together, this is the final result:
let blocks = document.querySelectorAll("pre");
blocks.forEach((block) => {
if (!navigator.clipboard) {
return;
}
let button = document.createElement("button");
button.className = "button-copy-code";
button.innerHTML = copyIcon;
block.appendChild(button);
button.addEventListener("click", async () => {
await copyCode(block);
});
});
async function copyCode(block) {
let copiedCode = block.cloneNode(true);
copiedCode.removeChild(copiedCode.querySelector("button.button-copy-code"));
const html = copiedCode.outerHTML.replace(/<[^>]*>?/gm, "");
block.querySelector("button.button-copy-code").innerHTML = copiedIcon;
setTimeout(function () {
block.querySelector("button.button-copy-code").innerHTML = copyIcon;
}, 2000);
const parsedHTML = htmlDecode(html);
await navigator.clipboard.writeText(parsedHTML);
}
function htmlDecode(input) {
const doc = new DOMParser().parseFromString(input, "text/html");
return doc.documentElement.textContent;
}
Styling the copy
button
As in GitHub, I want to position the botton in the top corner of the pre
tag. To achieve this, we must set the pre
as position: relative
, with the following
css
code:
pre[class*="shiki"] {
position: relative;
margin: 5px 0;
padding: 1.75rem 0 1.75rem 1rem;
}
Then, we need to set the button
as position: absolute
, so we can set the top
and right
properties:
.markup-docs > pre > .button-copy-code {
@apply rounded;
@apply bg-gray-300;
@apply py-2 px-2;
position: absolute;
top: 85px;
right: 15px;
}
@screen sm {
.markup-docs > pre > .button-copy-code {
top: 85px;
right: 10px;
}
}
With that in place, we can add a little bit of visual feedback to the button
.markup-docs > pre > .button-copy-code:hover {
@apply border-2 border-gray-600;
@apply bg-gray-200;
}
.markup-docs > pre > .button-copy-code:focus {
@apply bg-gray-300;
}
.copy-docs-icon {
fill: #0a001f;
}
.docs-copied-icon {
color: #148a25 !important;
fill: #148a25 !important;
}
In closing
The copy button in code blocks is a great feature to improve the experience for the reader. Now we can copy and paste content from techinical docs into our project with the click of a button. This is the final result:
What are your thoughts on "How to add GitHub Copy to Clipboard button on your docs/blog"?