Skip to main content
null 💻 notes

How to add a code copy button to your Hugo template

17-Feb-2024: I wrote this a while ago when my site was built with Hugo. I've since migrated to 11ty, but the principles used here are mostly tool-agnostic.

Due to the way code snippets are rendered differently when they contain line numbers, implementing a code copy button will depend on whether or not you have configured your snippets to contain line numbers.

See the comments in the javascript code below to determine which version you'll need to use for your site.

In your theme's assets/js folder, create code-copy-button.js:

function createCopyButton(highlightDiv) {
  if (highlightDiv.classList.contains("nocopybutton")) {
    return;
  }

  const wrapper = document.createElement("div");
  wrapper.className = "highlight-wrapper";
  highlightDiv.parentNode.insertBefore(wrapper, highlightDiv);

  const button = document.createElement("button");
  button.className = "copy-code-button";
  button.type = "button";
  button.innerText = "Copy";
  button.addEventListener(
    "click",
    () => copyCodeToClipboard(button, highlightDiv),
  );

  wrapper.appendChild(button);
  wrapper.appendChild(highlightDiv);
}

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

async function copyCodeToClipboard(button, highlightDiv) {
  // With line numbers:
  const codeToCopy = highlightDiv.querySelector(
    "div > table > tbody > tr > td:nth-child(2) > pre > code",
  ).innerText.replaceAll("\n\n", "\n");

  try {
    var 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 {
    button.blur();
    button.innerText = "✔️";
    setTimeout(function () {
      button.innerText = "Copy";
    }, 2000);
  }
}

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

In your theme's footer, often found in themes/<your_theme>/layouts/partials/ somewhere, add the following near the </body> tag:



{{ if (findRE "<pre" .Content 1) }}
  {{ $jsCopy := resources.Get "js/code-copy-button.js" | minify }}
  <script src="{{ $jsCopy.RelPermalink }}"></script>
{{ end }}

Styling your code copy button #

Here's what I'm using:

.highlight-wrapper {
  display: block;

  /* This is important! Without this, your code copy
  button will scroll along the x-axis in cases where code
  lines cause overflow. This keeps the code copy button
  stuck to the top right of the snippet: */
  position: relative;

  background-color: #fafafa;
  margin-bottom: 1.2rem;
}

.highlight-wrapper .lntd pre {
  padding: 0;
}

.chroma .lntd pre {
  border: none;
}

.chroma .lntd:first-child {
  padding: 7px 7px 7px 10px;
  margin: 0;
}

.chroma .lntd:last-child {
  padding: 7px 10px 7px 7px;
  margin: 0;
}

.highlight {
  position: relative;
  z-index: 0;
  padding: 0;
  border-radius: 4px;
  overflow-x: auto;
}

.highlight > .chroma {
  position: static;
  z-index: 1;
  border-radius: 0 0 4px 4px;
  padding: 10px;
}

.copy-code-button {
  position: absolute;
  z-index: 2;
  right: 0;
  top: 0px;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 14px;
  letter-spacing: 0.5px;
  width: 55px;
  color: #232326;
  background-color: #fafafa;
  border: 1.25px solid #232326;
  border-bottom-left-radius: 4px;
  white-space: nowrap;
  padding: 6px;
  margin: 0 0 0 1px;
  cursor: pointer;
  opacity: 0.45;
}

.copy-code-button:hover,
.copy-code-button:focus,
.copy-code-button:active,
.copy-code-button:active:hover {
  color: #222225;
  background-color: #b3b3b3;
  opacity: 1;
}

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

Hugo configuration #

Here's my Hugo configuration settings pertaining to code snippets:

[markup.highlight]
      codeFences = true
      guessSyntax = false
      hl_Lines = ""
      lineNoStart = 1
      lineNos = true
      lineNumbersInTable = true
      tabWidth = 4
      noClasses = false
      noHl = true
      style = 'monokailight'

[markup]
      [markup.goldmark]
            [markup.goldmark.renderer]
                  unsafe = true