3 min read
Adding a copy button to code blocks in an Astro blog

Copy code demo

In Astro, code fences (```) come with built-in syntax highlighting support for Shiki and Prism. These, however, do not come with built-in copy to clipboard buttons which can make repeatedly copying code cumbersome.

Luckily, adding a copy button to code blocks can be done with a couple of steps:

1. Create a Rehype Plugin

We’ll use a custom rehype-copy-button plugin to wrap each <pre><code> block inside a wrapper as well as insert the copy button.

In src/rehype/rehype-copy-button.js:

import { visit } from "unist-util-visit";
import { h } from "hastscript";

export default function rehypeCopyButton() {
  return (tree) => {
    visit(tree, "element", (node, index, parent) => {
      if (node.tagName === "pre" && node.children[0]?.tagName === "code") {
        const wrapper = h("div", { class: "code-wrapper" }, [
          node,
          h("button", { class: "copy-btn", type: "button" }, "Copy"),
        ]);
        parent.children[index] = wrapper;
      }
    });
  };
}

Then, enable this plugin in your astro.config.ts:

import rehypeCopyButton from "./src/rehype/rehype-copy-button";

export default defineConfig({
  ...,
  markdown: {
    rehypePlugins: [rehypeCopyButton],
  },
});

2. Add the CSS for the Copy Button

Add the following CSS to your stylesheet (global.css or similar):

.code-wrapper {
  position: relative;
  margin: 1.25rem 0;
}

.code-wrapper pre {
  overflow-x: auto;
  padding-right: 2.5rem;
}

.copy-btn {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  background: rgba(0, 0, 0, 0.6);
  color: white;
  border: none;
  padding: 0.25rem 0.5rem;
  border-radius: 0.25rem;
  font-size: 0.75rem;
  cursor: pointer;
  opacity: 0;
  transition:
    opacity 0.2s,
    background 0.2s;
}

.code-wrapper:hover .copy-btn {
  opacity: 1;
}

.copy-btn:hover {
  background: rgba(0, 0, 0, 0.8);
}

3. Add the JavaScript for Copy Functionality

Create src/scripts/copy-button.js:

function copyToClipboard(copyBtn, text) {
  navigator.clipboard.writeText(text).then(() => {
    copyBtn.innerText = "Copied!";
    setTimeout(() => (copyBtn.innerText = "Copy"), 2000);
  });
}

document.addEventListener("click", (e) => {
  if (!e.target.classList.contains("copy-btn")) return;

  const copyBtn = e.target;
  const wrapper = e.target.closest(".code-wrapper");
  const code = wrapper.querySelector("pre code");

  copyToClipboard(copyBtn, code.innerText);
});

Then include it in your Astro layout (e.g., src/layouts/PageLayout.astro):

<body>
  <slot />
  ...
  <script>
    import "/src/scripts/copy-button.js";
  </script>
</body>

Now, every code block in your markdown posts will have a Copy button that works instantly.

Zoom overlay