Adding a Copy Button and Language Tab to Code Blocks in Hugo

Copy Button

I wanted it to be easier for readers to copy the code in my code blocks, so I wanted to make a copy button for each code block.

I based my implementation on this one by aaronluna, but I made some modifications.

This is the JavaScript I use. I placed it towards the bottom of my <body> tag, since the script only needs to run after the code block is loaded.

function createCopyButton(highlightDiv) {
    const button = document.createElement("button");
    button.className = "copy-code-button unselectable";
    button.type = "button";
    button.innerText = "Copy";
    button.setAttribute("unselectable", "on");
    button.addEventListener("click", () => copyCodeToClipboard(button, highlightDiv));
    addCopyButtonToDom(button, highlightDiv);
}

async function copyCodeToClipboard(button, highlightDiv) {
    const codeToCopy = highlightDiv.querySelector(":last-child > .chroma > code").textContent; // Use textContent instead of innerText
    try {
        result = await navigator.permissions.query({ name: "clipboard-write" });
        if (result.state == "granted" || result.state == "prompt") {
            await navigator.clipboard.writeText(codeToCopy);
        } else {
            copyCodeBlockExecCommand(codeToCopy, highlightDiv);
        }
    } catch (_) {
        copyCodeBlockExecCommand(codeToCopy, highlightDiv);
    }
    finally {
        codeWasCopied(button);
    }
}

function copyCodeBlockExecCommand(codeToCopy, highlightDiv) {
    const textArea = document.createElement("textArea");
    textArea.contentEditable = 'true'
    textArea.readOnly = 'false'
    textArea.className = "copyable-text-area";
    textArea.value = codeToCopy;
    highlightDiv.insertBefore(textArea, highlightDiv.firstChild);
    const range = document.createRange()
    range.selectNodeContents(textArea)
    const sel = window.getSelection()
    sel.removeAllRanges()
    sel.addRange(range)
    textArea.setSelectionRange(0, 999999)
    document.execCommand("copy");
    highlightDiv.removeChild(textArea);
}

function codeWasCopied(button) {
    button.blur();
    button.innerText = "Copied!";
    setTimeout(function () {
        button.innerText = "Copy";
    }, 1000);
}

function addCopyButtonToDom(button, highlightDiv) {
    highlightDiv.insertBefore(button, highlightDiv.firstChild);
    const wrapper = document.createElement("div");
    wrapper.className = "highlight-wrapper";
    highlightDiv.parentNode.insertBefore(wrapper, highlightDiv);
    wrapper.appendChild(highlightDiv);
}

document.querySelectorAll(".highlight")
    .forEach(highlightDiv => createCopyButton(highlightDiv));

Unlike aaronluna’s solution, I use textContent instead of innerText, as innerText includes an additional newline for each line of code.

Here is my SCSS for the copy button (but you need to replace my CSS variables and SASS variables, the items that go like var(--XXX) and $XXX respectively).

.copy-code-button {
  position: absolute;
  right: 0px;
  top: 1px;
  transform: translateY(-100%);
  font-size: 13.5px;
  font-weight: 700;
  color: var(--text-color);
  background-color: var(--syntax-highlighting-background);
  border: none;
  white-space: nowrap;
  padding: 2px 12px 0px;
  margin: 0 0 0 1px;
  cursor: pointer;
  border-radius: 8px 8px 0px 0px;
  line-height: 1.7;
}

.copy-code-button:hover,
.copy-code-button:focus,
.copy-code-button:active,
.copy-code-button:active:hover {
  color: var(--text-alt-color);
  transition: all 0.2s;
}

.copyable-text-area {
  position: absolute;
  height: 0;
  z-index: -1;
  opacity: 0.01;
}

*.unselectable {
  -moz-user-select: -moz-none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

I place this in a SCSS file inside my assets/sass folder, but you might have a different pipeline for managing your CSS.

My SCSS also differs from aaronluna’s solution: I wanted to place my copy button on the top right, instead of within the code block. I didn’t want to worry that my copy button would cover the text in the block, which could happen if the lines were long.

Moreover, I wanted to make the copy button text unselectable, since I didn’t want the language tab text to be selectable either. More on the language tab in the next section:

Language Tab

I also noticed that the code blocks on aaronluna’s site have a tab showing the name of the coding language, which I think is a useful feature.

My method is based on aaronluna’s CSS. Since we are using Hugo to generate the site, this method will take advantage of the code blocks that Hugo already has.

In almost all my code blocks in my Markdown files, I already specify the language on top like this:

A screenshot showing how I specify the language in my Markdown code blocks.

So this method can just retrieve the language name, and show it on the top of the code blocks.

This is what I added to my SCSS:

.chroma [data-lang]:not([data-lang="fallback"]):before {
  position: absolute;
  z-index: 0;
  top: 1px;
  transform: translateY(-100%);
  font-family: $base-font-family;
  font-weight: 700;
  content: attr(data-lang);
  padding: 2px 12px 0px;
  border-radius: 8px 8px 0px 0px;
  text-transform: uppercase;
  background-color: var(--syntax-highlighting-background);
}

And it works! Now my code blocks are fancier!