roger pence

Potentially, each day is crucial in the total development

Search
Advanced Search
Folder to search:
Limit search to one of these properties:

    How to add a copy-code button to Eleventy using Shiki

    Copy Button for Code Fences

    This document explains how the copy button was added to fenced code blocks in this Eleventy site, and how to change the icon later if you want a different look. This copy button doesn't require any extra packages—it's all plain vanilla JavaScript.

    What the feature does

    Each code fence now gets:

    • a small copy button in the upper-right corner
    • the original source code stored on the <pre> element as data-copy-source
    • a click handler that copies the raw fence content to the clipboard
    • a small wiggle animation after the button is clicked

    The clipboard text comes from the original fence source (stored as a value of the data-copy-source attribute), not from the rendered Shiki HTML. This bloats the HTML a bit, but the tradeoff is that it provides easier and much cleaner copy results.

    Files involved

    The feature lives in four places:

    • .eleventy.js - adds the Shiki transformer and stores the raw source text
    • src/scripts/code-copy.js - handles the clipboard copy and click animation
    • src/css-dev/main.css - positions and animates the button
    • src/_includes/footer.njk - loads the script site-wide

    Step 1: Add a Shiki transformer

    The code fence renderer is configured in .eleventy.js through eleventyConfig.amendLibrary("md", ...). The key change is a Shiki transformer passed into codeToHtml().

    The transformer does three things in its pre() hook:

    1. adds a copyable-code class to the <pre> wrapper
    2. stores the original fence source in data-copy-source
    3. prepends a button element that contains the copy icon and provides the copy action

    Every fenced block renders its own button without needing a separate DOM scan after page load. The raw source is available inside the transformer as this.source. That is the exact code from the code fence text before Shiki turns it into highlighted HTML, so the clipboard receives the original code and not the markup.

    Step 2: Add the client-side copy handler

    src/scripts/code-copy.js listens for clicks on .code-copy-button.

    When the user clicks the button, the script:

    1. Finds the closest pre[data-copy-source]
    2. Reads the raw code from dataset.copySource
    3. Copies it with navigator.clipboard.writeText() when available
    4. Falls back to a hidden <textarea> plus document.execCommand("copy") when needed
    5. Adds an is-wiggling class so the animation plays
    6. Removes the class after a short delay

    Clipboard fallback

    The modern Clipboard API is used first. If the browser blocks it or does not support it, the script falls back to the classic hidden-textarea approach. That makes the feature more resilient across browsers.

    Step 3: Style the button

    The button styling lives in src/css-dev/main.css.

    The main CSS rules do the following:

    • make the <pre> wrapper position: relative
    • absolutely position the button in the upper-right corner
    • give the button a circular shape and a subtle shadow
    • keep the icon small and centered
    • define the wiggle animation with @keyframes code-copy-wiggle

    The effect is intentionally small so it feels like part of the code block rather than a separate control.

    Step 4: Load the script on the page

    The script is loaded from src/_includes/footer.njk:

    <script type="module" src="/scripts/code-copy.js"></script>
    

    Placing it in the footer keeps the markup simple and makes sure the handler is available on every page that renders code fences.

    How the icon is defined

    The copy icon is defined directly in .eleventy.js inside createClipboardIconNode().

    It is an inline SVG node with a path element that draws the clipboard shape.

    That means the icon is not coming from a package, an image file, or an icon font. It is part of the rendered button itself.

    If you want a different SVG, replace the path data inside createClipboardIconNode() with the new icon's viewBox, path, and any stroke/fill values it needs. The button will keep using currentColor, so the icon color still follows the button text color unless you change the SVG properties.

    How to change the icon

    To change the icon, edit the SVG node in .eleventy.js.

    The function to update

    Look for this helper:

    const createClipboardIconNode = () => ({
        type: "element",
        tagName: "svg",
        properties: {
            viewBox: "0 0 24 24",
            fill: "none",
            stroke: "currentColor",
            "stroke-width": 1.8,
            "stroke-linecap": "round",
            "stroke-linejoin": "round",
            "aria-hidden": "true",
            focusable: "false",
        },
        children: [
            {
                type: "element",
                tagName: "path",
                properties: {
                    d: "...",
                },
                children: [],
            },
        ],
    });
    

    What you can change

    You can update any of these parts:

    • viewBox - adjust this if your new SVG uses a different coordinate system
    • stroke-width - make the outline thicker or thinner
    • d - replace the actual icon path data
    • fill - switch to a filled icon if you prefer
    • stroke - change the icon color behavior

    If the new SVG has a different size or alignment, update the button icon rule in src/css-dev/main.css as well:

    .tech-post-wrapper pre.copyable-code .code-copy-button svg {
        width: 1rem;
        height: 1rem;
    }
    

    That keeps the icon visually balanced inside the button.

    Example: replace the path

    If you want a simpler icon, you can swap the path data for another SVG path. For example, a checkmark or a plus sign would only require changing the d value.

    Example: use a filled icon

    If the new icon should be filled instead of outlined, change the SVG properties and shape accordingly:

    properties: {
        viewBox: "0 0 24 24",
        fill: "currentColor",
        stroke: "none",
        "aria-hidden": "true",
        focusable: "false",
    }
    

    Then replace the child path with the new filled icon path.

    If you want a different size

    The icon size is controlled by CSS in src/css-dev/main.css:

    .tech-post-wrapper pre.copyable-code .code-copy-button svg {
        width: 1rem;
        height: 1rem;
    }
    

    You can change those values if the icon should be larger or smaller.

    If you want to change the button appearance

    The button itself is styled in src/css-dev/main.css under .tech-post-wrapper pre.copyable-code .code-copy-button.

    Useful tweaks include:

    • changing width and height
    • changing border-radius for a less circular button
    • adjusting box-shadow for more or less depth
    • changing opacity for a softer or more visible button
    • changing top and right to reposition it

    Troubleshooting

    The button shows up, but click does nothing

    Check that src/scripts/code-copy.js is loaded in the footer and that the browser allows clipboard access.

    The button copies HTML instead of source text

    That usually means the source is not being read from data-copy-source. The transformer in .eleventy.js should add that attribute to the <pre> element.

    The wiggle is too subtle

    Increase the rotation or add more translation in @keyframes code-copy-wiggle in src/css-dev/main.css.

    The icon does not change

    Make sure you edited createClipboardIconNode() in .eleventy.js, then reload the page and rebuild the site if needed.

    Summary

    The feature is built without third-party clipboard packages. Shiki injects the button and raw source, a small vanilla script handles the clipboard action, and CSS handles placement plus the wiggle animation. To change the icon, update the inline SVG helper in .eleventy.js.