Skip to content

feat: add copy button to code blocks - #127

Merged
baymac merged 6 commits into
mainfrom
baymac/copy-button-code-blocks
Jun 13, 2026
Merged

feat: add copy button to code blocks#127
baymac merged 6 commits into
mainfrom
baymac/copy-button-code-blocks

Conversation

@baymac

@baymac baymac commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a one-click copy button to every <pre> block in blog posts — clicking copies the snippet to clipboard and shows a checkmark for 2 s before resetting
  • Sets tabindex="0" on <pre> and <table> elements from Markdown for keyboard accessibility
  • Replaces @iconscout/react-unicons npm package with a local components/Icons/UilIcons.tsx bundle (removes ~5 KB JS / ~14 KB CSS from first load)

Implementation

lib/posts.tsrehypeCopyButton plugin (exported)
Visits hast nodes at build time, injecting a <button class="copy-btn"> with two icon spans (copy-icon / check-icon) into each <pre>, and tabindex="0" onto <pre> and <table> nodes.

components/Blog/PostContent.tsx → server component
Renders the HTML string with data-post-content attribute and mounts <CopyButtonHandler />.

components/Blog/CopyButtonHandler.tsx'use client'
Thin client shell: attaches a delegated click listener to [data-post-content], manages per-button setTimeout dedup (rapid re-click restarts the timer), cleans up on unmount.

styles/pageStyles/blog.module.css
Copy button CSS — hover/focus states, copied feedback colour, dark-theme override.

Tests

28 new tests across 2 files (91 total, 0 fail):

  • tests/unit/lib/rehypeCopyButton.test.ts — 12 tests driving the real plugin through the full remark → rehype → highlight → stringify pipeline
  • tests/unit/components/PostContent.test.tsx — 16 tests covering clipboard success/failure, timer reset (fake timers), delegated click, code text extraction, unmount cleanup

Test plan

  • Click a copy button on any blog post — snippet copies to clipboard
  • Checkmark appears for ~2 s, then button resets
  • Rapid re-click restarts the timer (no stacked timeouts)
  • Clipboard denied (e.g. incognito) — button stays in default state
  • Tab to <pre> — it receives focus (tabindex="0")
  • Dark mode — copied state uses green (#2da44e)

🤖 Generated with Claude Code

baymac and others added 3 commits June 13, 2026 01:25
…-unicons

Replaces the npm package with a self-contained components/Icons/UilIcons.tsx
that re-exports only the icons in use, shaving ~5 KB off the JS bundle and ~14 KB
off first-load CSS. Updates all consumers (Nav, Footer, Modal, BuyMeCrypto,
BlogButton) and removes BlogListClient/Skeleton components that were replaced by
the Postit redesign.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… pre/table

Adds a one-click copy button to every <pre> block rendered in blog posts.
Clicking copies the snippet to the clipboard and briefly shows a checkmark
(2 s) before reverting. Also sets tabindex="0" on <pre> and <table> elements
for keyboard accessibility.

Implementation:
- rehypeCopyButton(): new exported rehype plugin in lib/posts.ts — visits hast
  nodes at build time, injecting the button and tabindex into the HTML string
- PostContent.tsx: server component that renders the HTML and mounts
  CopyButtonHandler for the interactive layer
- CopyButtonHandler.tsx: thin 'use client' component — attaches a delegated
  click listener to [data-post-content], manages per-button setTimeout dedup,
  and cleans up on unmount
- blog.module.css: copy button styles including hover/copied states and
  dark-theme colour overrides

Tests: 28 new tests (16 PostContent/CopyButtonHandler, 12 rehypeCopyButton)
covering the full remark → rehype → highlight → stringify pipeline, clipboard
success/failure paths, timer reset, delegation, and unmount cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blog Ready Ready Preview, Comment Jun 13, 2026 6:41pm

Move overflow-x scrolling from <pre> onto the inner <code>. The <pre> was
both the positioning anchor and the scroll container, so the absolutely
positioned button was anchored to the content box and scrolled away with
the code. <pre> is now a non-scrolling anchor; <code> handles the scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sable

The previous commit moved horizontal scroll from <pre> to the inner <code>,
but left tabindex="0" on the now-non-scrolling <pre>. axe-core flagged the
scrollable <code> as a keyboard trap (WCAG 2.1.1 scrollable-region-focusable),
failing the e2e a11y suite. Put tabindex on the <code> (the scroll container)
instead; the <pre> tabindex would just be a dead tab stop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@baymac
baymac merged commit 9fe37b7 into main Jun 13, 2026
6 checks passed
@baymac
baymac deleted the baymac/copy-button-code-blocks branch June 13, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant