How to add GitHub Copy to Clipboard button on your docs/blog

Original – by Mateus Junges – 5 minute read

If you usually use GitHub Markdown, you may have seen the small copy button at the top right corner of code blocks:

The GitHub copy button

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:

Final result of this tutorial

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:

Copy button befor copying the content Copy buton after the text was copied

Enjoying this blog?

Please consider Sponsoring me on GitHub.

My mission is to spend more time maintaining and creating more projects that I've written over the years and continuing developing new projects to make PHP development more productive and enjoyable.

Comments

What are your thoughts on "How to add GitHub Copy to Clipboard button on your docs/blog"?

Comments powered by Laravel Comments
Want to join the conversation? Log in or create an account to post a comment.