the home site for me: also iteration 3 or 4 of my site
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add copy code post

+226
+225
content/blog/2025-03-18_adding-a-copy-button.md
··· 1 + +++ 2 + title = "Adding a copy code button" 3 + date = 2025-03-14 4 + slug = "adding-a-copy-button" 5 + description = "continuing the chain :)" 6 + 7 + [taxonomies] 8 + tags = ["accessibility"] 9 + +++ 10 + 11 + It took me a little over a month but I finally continued the chain of adding copy code buttons to your code blocks. It started with Salma Alam-Naylor’s [post](https://whitep4nth3r.com/blog/how-to-build-a-copy-code-snippet-button/) which I saw on Hacker News but then [David Bushell](https://dbushell.com/2025/02/14/copy-code-button/) also posted on it and [Ragman](https://www.ragman.net/musings/copy_code/) made a bluesky post (sky? bloop? atproto bloop? honestly not sure what a more interesting name would be) and it's been saved in my mind since then that I should add it. 12 + 13 + <!-- more --> 14 + 15 + What finally pushed me over the edge was seeing the [Duckquill](https://duckquill.daudix.one) theme and its fancy code blocks. I cloned the theme (`git clone https://codeberg.org/daudix/duckquill.git`) and figured out that the actual copy code was some reasonably simple js in `static/copy-button.js`. I copied that file and messed with it a bit as well as the css (`sass/_pre-container.scss` and some icon stuff in `sass/_icon.scss`) to make it work with my theme and style. 16 + 17 + A quick hash for cache busting and import later it all worked! 18 + 19 + > templates/head.html 20 + ```html 21 + {% set jsHash = get_hash(path="js/copy-button.js", sha_type=256, 22 + base64=true) %} 23 + <script 24 + src="{{ get_url(path='js/copy-button.js?' ~ jsHash, trailing_slash=false) | safe }}" 25 + defer 26 + ></script> 27 + ``` 28 + 29 + The one thing I expanded on was the ability to specify a file name / comment for the code block. When js is disabled a markdown `>` blockquote on the line before the code block will create a header tab for the code block. I snipped the header tab idea from [chevyray.dev](https://chevyray.dev) and I grew to quite like it so I didn't want to abandon it over a copy button. 30 + 31 + Here is my code should you want to use it: 32 + 33 + > static/js/copy-button.js 34 + ```js 35 + // Based on https://www.roboleary.net/2022/01/13/copy-code-to-clipboard-blog.html 36 + document.addEventListener("DOMContentLoaded", () => { 37 + const blocks = document.querySelectorAll("pre[class^='language-']"); 38 + 39 + for (const block of blocks) { 40 + if (navigator.clipboard) { 41 + // Code block header title 42 + const title = document.createElement("span"); 43 + const lang = block.getAttribute("data-lang"); 44 + const comment = 45 + block.previousElementSibling && 46 + (block.previousElementSibling.tagName === "blockquote" || 47 + block.previousElementSibling.nodeName === "BLOCKQUOTE") 48 + ? block.previousElementSibling 49 + : null; 50 + if (comment) block.previousElementSibling.remove(); 51 + title.innerHTML = 52 + lang + (comment ? ` (${comment.textContent.trim()})` : ""); 53 + 54 + // Copy button icon 55 + const icon = document.createElement("i"); 56 + icon.classList.add("icon"); 57 + 58 + // Copy button 59 + const button = document.createElement("button"); 60 + const copyCodeText = "Copy code"; // Use hardcoded text instead of getElementById 61 + button.setAttribute("title", copyCodeText); 62 + button.appendChild(icon); 63 + 64 + // Code block header 65 + const header = document.createElement("div"); 66 + header.classList.add("header"); 67 + header.appendChild(title); 68 + header.appendChild(button); 69 + 70 + // Container that holds header and the code block itself 71 + const container = document.createElement("div"); 72 + container.classList.add("pre-container"); 73 + container.appendChild(header); 74 + 75 + // Move code block into the container 76 + block.parentNode.insertBefore(container, block); 77 + container.appendChild(block); 78 + 79 + button.addEventListener("click", async () => { 80 + await copyCode(block, header, button); // Pass the button here 81 + }); 82 + } 83 + } 84 + 85 + async function copyCode(block, header, button) { 86 + const code = block.querySelector("code"); 87 + const text = code.innerText; 88 + 89 + await navigator.clipboard.writeText(text); 90 + 91 + header.classList.add("active"); 92 + button.setAttribute("disabled", true); 93 + 94 + header.addEventListener( 95 + "animationend", 96 + () => { 97 + header.classList.remove("active"); 98 + button.removeAttribute("disabled"); 99 + }, 100 + { once: true }, 101 + ); 102 + } 103 + }); 104 + ``` 105 + 106 + and the css: 107 + 108 + > sass/css/_copy-button.scss 109 + ```scss 110 + i.icon { 111 + display: inline-block; 112 + mask-size: cover; 113 + background-color: currentColor; 114 + width: 1rem; 115 + height: 1rem; 116 + font-style: normal; 117 + font-variant: normal; 118 + line-height: 0; 119 + text-rendering: auto; 120 + } 121 + 122 + .pre-container { 123 + --icon-copy: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' height='16' width='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 3c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3 0 .55-.45 1-1 1s-1-.45-1-1c0-.57-.43-1-1-1H3c-.57 0-1 .43-1 1v5c0 .57.43 1 1 1 .55 0 1 .45 1 1s-.45 1-1 1c-1.645 0-3-1.355-3-3zm5 5c0-1.645 1.355-3 3-3h5c1.645 0 3 1.355 3 3v5c0 1.645-1.355 3-3 3H8c-1.645 0-3-1.355-3-3zm2 0v5c0 .57.43 1 1 1h5c.57 0 1-.43 1-1V8c0-.57-.43-1-1-1H8c-.57 0-1 .43-1 1m0 0'/%3E%3C/svg%3E"); 124 + --icon-done: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M7.883 0q-.486.008-.965.074a7.98 7.98 0 0 0-4.602 2.293 8.01 8.01 0 0 0-1.23 9.664 8.015 8.015 0 0 0 9.02 3.684 8 8 0 0 0 5.89-7.75 1 1 0 1 0-2 .008 5.986 5.986 0 0 1-4.418 5.816 5.996 5.996 0 0 1-6.762-2.766 5.99 5.99 0 0 1 .922-7.25 5.99 5.99 0 0 1 7.239-.984 1 1 0 0 0 1.363-.371c.273-.48.11-1.09-.371-1.367A8 8 0 0 0 9.492.14 8 8 0 0 0 7.882 0m7.15 1.998-.1.002a1 1 0 0 0-.687.34L7.95 9.535 5.707 7.29A1 1 0 0 0 4 8a1 1 0 0 0 .293.707l3 3c.195.195.465.3.742.293.277-.012.535-.133.719-.344l7-8A1 1 0 0 0 16 2.934a1 1 0 0 0-.34-.688 1 1 0 0 0-.627-.248'/%3E%3C/svg%3E"); 125 + 126 + margin: 1rem 0 1rem; 127 + border-radius: 0.75rem; 128 + 129 + .header { 130 + display: flex; 131 + justify-content: space-between; 132 + align-items: center; 133 + border-radius: 0.2em 0.2em 0 0; 134 + background-color: var(--accent); 135 + background-size: 200%; 136 + padding: 0.25rem; 137 + height: 2.5rem; 138 + 139 + span { 140 + margin-inline-start: 0.75rem; 141 + color: var(--purple-gray); 142 + font-weight: bold; 143 + line-height: 1; 144 + } 145 + 146 + button { 147 + appearance: none; 148 + transition: 200ms; 149 + cursor: pointer; 150 + border: none; 151 + border-radius: 0.4rem; 152 + background-color: transparent; 153 + padding: 0.5rem; 154 + color: var(--purple-gray); 155 + line-height: 0; 156 + 157 + &:hover { 158 + background-color: color-mix( 159 + in oklab, 160 + var(--accent) 80%, 161 + var(--purple-gray) 162 + ); 163 + } 164 + 165 + &:focus { 166 + background-color: color-mix( 167 + in oklab, 168 + var(--accent) 80%, 169 + var(--purple-gray) 170 + ); 171 + } 172 + 173 + &:active { 174 + transform: scale(0.9); 175 + } 176 + 177 + &:disabled { 178 + cursor: not-allowed; 179 + 180 + &:active { 181 + transform: none; 182 + } 183 + } 184 + 185 + .icon { 186 + -webkit-mask-image: var(--icon-copy); 187 + mask-image: var(--icon-copy); 188 + transition: 200ms; 189 + 190 + :root[dir*="rtl"] & { 191 + transform: scaleX(-1); 192 + } 193 + } 194 + } 195 + 196 + &.active { 197 + button { 198 + animation: active-copy 0.3s; 199 + 200 + color: var(--purple-gray); 201 + 202 + .icon { 203 + -webkit-mask-image: var(--icon-done); 204 + mask-image: var(--icon-done); 205 + } 206 + } 207 + 208 + @keyframes active-copy { 209 + 50% { 210 + transform: scale(0.9); 211 + } 212 + 100% { 213 + transform: none; 214 + } 215 + } 216 + } 217 + } 218 + 219 + pre { 220 + margin: 0; 221 + box-shadow: none; 222 + border-radius: 0 0 0.2em 0.2em; 223 + } 224 + } 225 + ```
+1
templates/head.html
··· 91 91 src="{{ get_url(path='js/copy-button.js?' ~ jsHash, trailing_slash=false) | safe }}" 92 92 defer 93 93 ></script> 94 + 94 95 <script> 95 96 function cb(res) { 96 97 const fmt = new Intl.NumberFormat('en', { notation: 'compact' });