🇵🇸 Donate eSIMs to Gaza 🇵🇸

Adding a Copy Code Button

A screenshot of the code I use to add the Copy Code buttons to my blog. Scroll down to read the actual code later in the article.
Improving accessibility, one feature at a time.

I just read David Bushell’s blog post about reading Salma Alam-Naylor’s blog post about adding copy code buttons, and decided to continue the chain.

By “copy code button”, we mean buttons added to code snippets that copy the text to your clipboard when clicked.

For example:

const x = 1 + 1;

If you click that “Copy Code” above, you should have const x = 1 + 1; in your copy/paste clipboard.

This is a feature that I’d seen on other blog posts, but it wasn’t until reading Alam-Naylor’s post that I understood what a help it is for people that use voice commands instead of a mouse.

Quoting from Alam-Naylor:

It is extremely difficult to highlight blocks of code and copy them with voice commands. At this point, for me at least, it is impossible. (Either it is actually impossible, or I haven’t had the brainpower to work out how to do it yet. Learning to navigate the web and code without your hands takes a huge mental toll.) For this reason, when you’re writing about code, especially when you’re creating tutorials that involve the reader copying and pasting code, it is absolutely imperative that you include a way for people with limited use of their hands to copy that code in a single action.

As somebody who also has a (less servere) form of Carpal Tunnel this hits particularly hard for me.

I want to make this site as easy as possible for people read and use, including those who can’t use a keyboard and mouse.

After all, everybody’s going to end up “disabled” at some point or another, if only just because of old age. We owe it to the people around us and our future selves to try to make our world accessible.

Technical Details

I write my musings and notes in Markdown, and use pandoc to convert them into HTML.

Pandoc has a concept of filters, a standardized way to write code against Pandoc’s Abstract Syntax Tree (AST). Basically, it lets you write code to change your HTML output at generation time.

I already use a Pandoc filter to turn inline links into footnotes, to allow for posting to Mastodon, and so I figured adding one more was the way to go!

Here’s the Python filter I ended up writing, which takes any CodeBlocks and adds a <button> below them.

#!/opt/homebrew/bin/python3

from hashlib import sha256

from pandocfilters import toJSONFilter, CodeBlock, RawBlock, Str, Plain

# Cache needed to prevent infinite recursion: https://github.com/jgm/pandocfilters/issues/72
alreadySeen = {}

def addCopyCodeButton(key, value, _format, _meta):
    if key == "CodeBlock" and not alreadySeen.get(value[1]):
        [id_, classes, namevals], contents = value
        alreadySeen[value[1]] = True

        hash = sha256(contents.encode()).hexdigest()

        buttonHTML = "<button class=\"copy-code-button\" onclick=\"navigator.clipboard.writeText(document.getElementById('" + hash + "').textContent).then(() => {this.textContent='Copied!';})\">"

        return [CodeBlock([hash, classes, namevals], contents), RawBlock("html", buttonHTML), Plain([Str("Copy Code")]), RawBlock("html", "</button>")]

if __name__ == "__main__":
    toJSONFilter(addCopyCodeButton)

This is basically entirely cribbed from Bushell’s solution but it does it at HTML generation time instead of with Javascript on page load.

Each code block gets its own HTML id, generated from the sha256 hash of the content.1

Then when the copy button is clicked, it looks up the code block by id, gets its textContent, and puts it in your clipboard.

The button’s own text is then changed to “Copied!”.

Only tricky thing to note is that Python pandocfilters module has a bug where your filter returns a new object, and then the filter gets run again against that new object.

In my case, our filter takes in a CodeBlocks and returns a new CodeBlock, and then the filter gets run against the new CodeBlock, so a new new CodeBlock is returned, and the filter gets run against that… on and on until we hit the recursion limit.

I got around this in my filter by added a cache, and skipping over a CodeBlock if we’ve already seen its contents before.

I did look into fixing the bug in the module itself, and it turns out that somebody made a PR for it in 2023. Hopefully that gets merged in at some point!


  1. This does mean that if I had two codeblocks with the same content, they’d end up with the same id. This would be a violation of the HTML contract, where ids need to be unique. It’d also break my pandoc filter recursion bug fix.

    Not going to bother designing around it though – this be my blog after all!↩︎