Beginner

Copy Code Button

A real copy button on every code block on this site, added by one script, with no per-page setup.

Live Implementation

Hover the code block below and a copy button appears in the top-right corner. This isn't a special widget — it's the exact same script that put a copy button on every other code block on this entire site, including the "Important Files" section further down this page:

const greeting = "Copy me with the button above";
console.log(greeting);

How It Works

One script, loaded sitewide through main.js, finds every <pre> on the page and wraps each one:

document.querySelectorAll("pre").forEach((pre) => {
    const wrapper = document.createElement("div");
    wrapper.className = "code-block-wrapper";
    pre.parentNode.insertBefore(wrapper, pre);
    wrapper.appendChild(pre);
    // ...button gets appended to wrapper, not pre
});

The wrapper matters more than it looks. Code blocks scroll horizontally (overflow-x: auto), and a button positioned inside a scrolling element can get clipped or scroll away with the content. Putting the button in a sibling wrapper instead — positioned relative to the wrapper, not the pre — keeps it pinned in the corner regardless of how far the code scrolls.

Clicking the button copies pre’s real text content via navigator.clipboard.writeText(), swaps the icon to a checkmark, and swaps it back after two seconds:

await navigator.clipboard.writeText(code.innerText);
button.classList.add("is-copied");
button.innerHTML = CHECK_ICON;
setTimeout(() => {
    button.classList.remove("is-copied");
    button.innerHTML = COPY_ICON;
}, 2000);

If navigator.clipboard doesn’t exist — an old browser, or a page loaded over plain HTTP instead of HTTPS — the whole script does nothing, on purpose. No button ever appears that wouldn’t actually work.

Folder Structure

src/assets/js/copy-code.js       ← the whole feature, loaded sitewide via main.js
src/assets/scss/components/_code.scss ← .code-block-wrapper and .copy-code-button styles

Important Files

src/assets/js/copy-code.js

const COPY_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="8" y="8" width="12" height="12" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M6 16H5a1.5 1.5 0 0 1-1.5-1.5v-9A1.5 1.5 0 0 1 5 4h9A1.5 1.5 0 0 1 15.5 5.5V6" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>`;
const CHECK_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M7.5 12.5l3 3 6-6.5"/></svg>`;

if (navigator.clipboard) {
    document.querySelectorAll("pre").forEach((pre) => {
        const wrapper = document.createElement("div");
        wrapper.className = "code-block-wrapper";
        pre.parentNode.insertBefore(wrapper, pre);
        wrapper.appendChild(pre);

        const button = document.createElement("button");
        button.type = "button";
        button.className = "copy-code-button";
        button.setAttribute("aria-label", "Copy code");
        button.innerHTML = COPY_ICON;
        wrapper.appendChild(button);

        button.addEventListener("click", async () => {
            const code = pre.querySelector("code") || pre;
            try {
                await navigator.clipboard.writeText(code.innerText);
                button.classList.add("is-copied");
                button.setAttribute("aria-label", "Copied");
                button.innerHTML = CHECK_ICON;
                setTimeout(() => {
                    button.classList.remove("is-copied");
                    button.setAttribute("aria-label", "Copy code");
                    button.innerHTML = COPY_ICON;
                }, 2000);
            } catch {}
        });
    });
}

Notes

  • This applies to literally every <pre> on the site, including the JSON dump on the Data Files demo and every “Important Files” snippet everywhere — not just fenced code blocks in Markdown.
  • The Clipboard API requires a secure context. It works on localhost during development and on the real deployed HTTPS site, but would silently do nothing if this were ever served over plain HTTP.
  • No dependency was added for this. navigator.clipboard has been in every evergreen browser for years — reaching for a library here would just be extra weight for something the platform already does.