How to add a code copy button to your Hugo template

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');
  
  // Without line numbers:
  // 


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

The demo is the “Copy” button you see in the code snippets on my site. Copy this or modify it to your liking.

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