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