How to add copy-to-clipboard to code blocks in JS in WordPress

Contents

Introduction

This article shows, in exhaustive detail, how to add a robust copy-to-clipboard feature for code blocks on a WordPress site using JavaScript. It covers all commonly needed approaches (theme functions.php enqueue, lightweight plugin, compatibility with common highlighters such as Prism / Highlight.js / EnlighterJS), accessibility, localization, fallback behavior for older browsers, keyboard support, performance considerations, styling, and troubleshooting.

What this implements

  • Button appended to code blocks (pre gt code or .language-… elements).
  • Clipboard API first, fallback to document.execCommand(copy) for older browsers.
  • Accessible — ARIA live region, keyboard-focusable button, visible feedback.
  • Localizable via wp_localize_script or passing strings from PHP.
  • Selector support for common highlighters and raw pre/code markup.
  • Minimal DOM changes using event delegation for scalability.

Prerequisites

  • Basic familiarity with adding code to a WordPress themes functions.php or creating a small plugin file.
  • Ability to add JS and CSS files to your theme or plugin and enqueue them correctly.
  • Knowledge of which code-highlighter plugin or library (if any) you use, so you can target the right selectors (Prism, Highlight.js, EnlighterJS, raw ltpregt tags, etc.).

Strategy and selectors

You generally want to attach one copy button per code block. Common selector patterns:

  • ltpregtltcodegt — plain HTML code blocks produced by editors or plugins.
  • .language-xxxxx — Prism-like classes placed on codes, e.g. ltpre class=language-jsgt or ltcode class=language-jsgt.
  • .hljs — Highlight.js wrapper class.
  • .EnlighterJSRAW or .EnlighterWrapper — EnlighterJS plugin classes.

The JavaScript will accept a configurable selector string so you can adapt it for your site.

Implementation — step by step

1) Create assets: JS and CSS

You will create two files: a JavaScript file (copy-to-clipboard.js) and a CSS file (copy-to-clipboard.css). The JS will:

  1. Find code blocks using a selector (configurable).
  2. Insert a copy button into each block or use event delegation with a single container.
  3. Use navigator.clipboard.writeText when available.
  4. Fallback to a hidden textarea document.execCommand(copy) if needed.
  5. Provide success/failure visual feedback and update accessible text.
  6. Optionally remove or hide line numbers when copying (if your highlighter includes them).

JavaScript example (complete client-side script). Place in copy-to-clipboard.js:

/
  copy-to-clipboard.js
  - Configurable selector: COPY_SELECTOR
  - Localized strings expected via window.CopyToClipboardStrings
/
(function () {
  use strict

  // Configuration
  var COPY_SELECTOR = pre > code, pre[class=language-], .highlight, .EnlighterJSRAW // adapt as needed
  var BUTTON_CLASS = copy-to-clipboard-btn
  var BUTTON_TEMPLATE = function (label) {
    // Using a button element text content we will set aria-label for screen readers
    return ltbutton type=button class=   BUTTON_CLASS    aria-live=politegt   label   lt/buttongt
  }

  // Localized strings fallback
  var STRINGS = (window.CopyToClipboardStrings  {
    copy: Copy,
    copied: Copied!,
    error: Copy failed
  })

  // Utility: create element from HTML string
  function createFromHTML(htmlString) {
    var div = document.createElement(div)
    div.innerHTML = htmlString.trim()
    return div.firstChild
  }

  // Copy text via Clipboard API or fallback
  function writeTextToClipboard(text) {
    if (!text) {
      return Promise.reject(new Error(No text to copy))
    }

    if (navigator.clipboard  navigator.clipboard.writeText) {
      return navigator.clipboard.writeText(text)
    }

    // Fallback using textarea   execCommand
    return new Promise(function (resolve, reject) {
      var textarea = document.createElement(textarea)
      // Move off-screen
      textarea.style.position = fixed
      textarea.style.top = -9999px
      textarea.setAttribute(readonly, )
      textarea.value = text
      document.body.appendChild(textarea)

      // Select and copy
      textarea.select()
      textarea.setSelectionRange(0, textarea.value.length)

      try {
        var successful = document.execCommand(copy)
        document.body.removeChild(textarea)
        if (successful) {
          resolve()
        } else {
          reject(new Error(execCommand returned false))
        }
      } catch (err) {
        document.body.removeChild(textarea)
        reject(err)
      }
    })
  }

  // Strip line numbers or UI injected by some highlighters (optional)
  function getCodeText(codeElement) {
    // If the highlighter stores text in a data attribute or child, adapt here.
    // Default: use innerText to preserve visible formatting
    return codeElement.innerText  codeElement.textContent  
  }

  // Feedback management: set temporary label and class
  function showFeedback(button, state) {
    // state: copied  error
    var original = button.getAttribute(data-original-label)  STRINGS.copy
    var label = (state === copied) ? STRINGS.copied : STRINGS.error
    button.textContent = label
    button.classList.add(is-   state)
    // revert after delay
    setTimeout(function () {
      button.textContent = original
      button.classList.remove(is-   state)
    }, 2000)
  }

  // Attach buttons: event delegation approach
  function attachCopyButtons(root) {
    if (!root) { root = document }

    // Ensure only one delegation gallery exists
    // We attach a single click listener to the document
    if (attachCopyButtons._attached) {
      return
    }
    attachCopyButtons._attached = true

    document.addEventListener(click, function (e) {
      var btn = e.target.closest(.   BUTTON_CLASS)
      if (!btn) { return }

      // Locate the associated code block. We expect the button to be placed as a child of the pre element.
      var container = btn.closest(pre)  btn.closest(figure)  btn.closest(.highlight)  btn.closest(.EnlighterWrapper)  null
      var codeEl = null
      if (container) {
        codeEl = container.querySelector(code)  container.querySelector(pre)  container
      } else {
        // If button was injected inline next to code
        codeEl = document.querySelector(COPY_SELECTOR)
      }

      if (!codeEl) {
        showFeedback(btn, error)
        return
      }

      var text = getCodeText(codeEl)

      writeTextToClipboard(text).then(function () {
        showFeedback(btn, copied)
        // Optionally, set focus back to button for keyboard users
        btn.focus()
      }).catch(function () {
        showFeedback(btn, error)
      })
    })
  }

  // Insert button(s) for each code block
  function init() {
    var nodes = document.querySelectorAll(COPY_SELECTOR)
    if (!nodes  !nodes.length) {
      return
    }

    // Add CSS class to prevent duplicate insertion
    var insertedCount = 0
    Array.prototype.forEach.call(nodes, function (node) {
      // Determine the parent 
 for robustness
      var pre = node.tagName.toLowerCase() === pre ? node : node.closest(pre)  node
      if (!pre) { return }

      if (pre.getAttribute(data-copy-inserted) === 1) {
        return
      }
      pre.setAttribute(data-copy-inserted, 1)

      // Create the button element
      var button = createFromHTML(BUTTON_TEMPLATE(STRINGS.copy))
      // accessibility labeling
      var langLabel = STRINGS.copy
      button.setAttribute(aria-label, langLabel)
      button.setAttribute(data-original-label, STRINGS.copy)
      button.classList.add(BUTTON_CLASS)

      // Insert into the pre element: prefer top-right (CSS will position)
      pre.insertBefore(button, pre.firstChild)
      insertedCount  
    })

    if (insertedCount > 0) {
      attachCopyButtons(document)
    }
  }

  // Init on DOMContentLoaded or immediately if already loaded
  if (document.readyState === complete  document.readyState === interactive) {
    setTimeout(init, 0)
  } else {
    document.addEventListener(DOMContentLoaded, init)
  }
})()

2) Styling the button

Add CSS to position and style the button. Put this in copy-to-clipboard.css:

/ copy-to-clipboard.css /
.copy-to-clipboard-btn {
  position: absolute
  top: 0.5rem
  right: 0.5rem
  z-index: 10
  background: rgba(0,0,0,0.6)
  color: #fff
  border: 0
  padding: 0.35rem 0.6rem
  font-size: 0.85rem
  border-radius: 3px
  cursor: pointer
  transition: background 0.12s ease, transform 0.08s ease
  line-height: 1
}

/ When placed inside a pre, ensure pre is positioned relatively /
pre {
  position: relative
}

/ focus styles for keyboard users /
.copy-to-clipboard-btn:focus {
  outline: 2px solid #ffd54a
  outline-offset: 2px
}

/ feedback states /
.copy-to-clipboard-btn.is-copied {
  background: #2e7d32 / green /
}

.copy-to-clipboard-btn.is-error {
  background: #c62828 / red /
}

3) Enqueue assets in WordPress

Use functions.php (or plugin main file) to enqueue the JS and CSS. Also use wp_localize_script to pass localized strings and any selector configuration.

Theme functions.php example (or plugin code):

 __( Copy, your-textdomain ),
        copied => __( Copied!, your-textdomain ),
        error  => __( Copy failed, your-textdomain ),
        // Optionally pass a custom selector
        // selector => pre > code, pre[class=language-], .highlight, .EnlighterJSRAW,
    )

    // Pass strings to the JS in a safe way
    wp_localize_script( my-copy-to-clipboard, CopyToClipboardStrings, strings )
}
add_action( wp_enqueue_scripts, mytheme_enqueue_copy_to_clipboard )
?>

If you want a plugin rather than theme, create a file like my-copy-to-clipboard.php with the standard plugin header and replace get_stylesheet_directory_uri() with plugin_dir_url( __FILE__ ).

4) Plugin version (single-file)

Minimal plugin main file (place in wp-content/plugins/copy-to-clipboard/copy-to-clipboard.php). This example uses the same asset structure but demonstrates a self-contained plugin approach:

 __( Copy, sctc ),
        copied => __( Copied!, sctc ),
        error  => __( Copy failed, sctc )
    )
    wp_localize_script( sctc-script, CopyToClipboardStrings, strings )
}
add_action( wp_enqueue_scripts, sctc_enqueue_assets )
?>

5) Integration with specific highlighters editor output

Different highlighters wrap code blocks differently. Adjust COPY_SELECTOR or refine the DOM traversal logic:

  • Prism: Pre tags usually have class language-xxx: selector pre[class=language-] gt code, pre[class=language-].
  • Highlight.js: code blocks often have class hljs on ltcodegt or ltpregt. Use .hljs or pre > code.hljs.
  • EnlighterJS: uses wrappers like .EnlighterWrapper or .EnlighterJSRAW for raw content. Use .EnlighterWrapper pre or .EnlighterJSRAW.
  • Gutenberg code block: the block may output ltpregtltcodegt. Use pre.wp-block-code or the general pre gt code fallback.

Accessibility details

  • Buttons are native ltbuttongt elements (keyboard-focusable by default).
  • Provide aria-label with the action and optionally include language text. We use aria-live on the button so changes are announced for more nuanced announcements, add a dedicated invisible aria-live region.
  • Ensure sufficient contrast for the button text background.
  • After copy, return focus to the button so keyboard-only users are not left without context.
  • If copying large blocks, consider notifying users of the size (e.g., Copied 80 lines).

Localization and strings

Use wp_localize_script to pass localizable UI strings into the JavaScript (see the PHP examples above). That allows translation via the WordPress translation system and keeps the JS clean.

Fallback and browser support

  • Primary: navigator.clipboard.writeText — modern, secure, asynchronous.
  • Fallback: create a temporary textarea, select and execute document.execCommand(copy). This is older and may be deprecated in some environments but works for many legacy browsers.
  • Note: navigator.clipboard.writeText may require HTTPS on some browsers. Always serve the site over HTTPS for reliable clipboard behavior.

Performance considerations

  • Prefer event delegation (single document click handler) to attaching individual listeners for every button if you have many code blocks.
  • Mark blocks with a data attribute (e.g., data-copy-inserted) to avoid duplicate insertion if your script runs multiple times (e.g., with PJAX or live previews).
  • Defer script loading (load in footer) so it doesnt block initial page rendering.

Troubleshooting and common pitfalls

  1. Button not visible — ensure pre has position:relative or adjust your CSS insertion strategy (you can append into a wrapper instead of pre firstChild).
  2. Clipboard API failing on HTTP — navigator.clipboard.writeText often requires TLS use HTTPS.
  3. Theme or plugin sanitizing inline buttons — if a content filter strips ltbuttongt from post HTML, make sure you inject the button DOM via JS rather than outputting it in server-rendered HTML.
  4. Line numbers included — some highlighters render line numbers as separate elements. Use codeElement.innerText may include them. If you want to strip them, update getCodeText() to ignore elements with a particular class (e.g., .line-numbers).
  5. Multiple runs duplicate buttons — use a flag like data-copy-inserted on the pre element.
  6. Screen reader not announcing copy result — add or update an aria-live=polite region and update its text on success/failure for a reliable announcement across ATs.

Advanced: copy only a selected range or line numbers

If you want users to copy only specific lines, the UI must allow selecting lines. Typical approaches:

  • Provide checkboxes or click-to-select lines and concatenate selected lines for copy.
  • Support copying a line range passed as data attributes or via small inputs (start/end).

This requires more complex DOM parsing and a UI to pick lines. For many sites, copying the entire visible code block is sufficient, but the advanced approach is possible by traversing child nodes and selecting only those with certain classes.

Complete end-to-end example

Below is a minimal working assembly of the PHP enqueue, JS logic, and CSS — combined pieces shown earlier, but presented here for quick copy-paste.

Functions.php enqueue (quick)

 __( Copy, textdomain ),
      copied => __( Copied!, textdomain ),
      error => __( Copy failed, textdomain ),
    )
    wp_localize_script( q-ctc-script, CopyToClipboardStrings, strings )
}
add_action( wp_enqueue_scripts, quick_enqueue_copy_to_clipboard )
?>

JavaScript (copy-to-clipboard.js)

/ (See the full JS provided earlier in this article) /
/ Use the self-invoking function above copy-paste exact code. /

CSS (copy-to-clipboard.css)

/ (See the CSS provided earlier in this article) /
/ Ensure pre { position: relative } exists so buttons absolutely-position correctly /

Testing checklist

  • Desktop: Chrome, Firefox, Safari — verify copy via Clipboard API.
  • Mobile: iOS Safari and Android Chrome — verify fallback behavior and that the copy action works (note iOS Clipboard API behavior varies by version).
  • Keyboard-only: tab to button, press Enter/Space, observe feedback and focus management.
  • Screen reader: test VoiceOver, NVDA, or JAWS to ensure copy success messages are announced.
  • Sites with caching/minification: ensure the JS file loads after minification and paths are correct test after clearing cache.

Security considerations

  • Do not read the clipboard (navigator.clipboard.readText) unless absolutely needed — reading requires explicit permission and raises privacy concerns.
  • Only copy text present in the DOM (code blocks). Do not generate content to copy that contains sensitive data.
  • Ensure your site is served over HTTPS to avoid Clipboard API restrictions.

Extending the feature

  • Add a small animation for a tick icon on success instead of text change.
  • Support copying with a keyboard shortcut (e.g., Ctrl Shift C) when a code block is focused — be mindful of conflicts.
  • Offer a Copy link to this code option that constructs a permalink line numbers and copies that instead.
  • Expose an action/filter in your plugin so other plugins/themes can modify selectors or text before initialization.

Summary

Adding copy-to-clipboard to code blocks in WordPress is straightforward: create a small JS file that inserts buttons and uses the Clipboard API with a safe fallback, add concise CSS for styling and positioning, and enqueue these assets using WordPress functions. Prioritize accessibility, localization, and robust selector handling for the different highlighter libraries. The approach in this article is production-ready and can be adapted into a theme or plugin easily.

If you need direct code snippets adapted to a particular highlighter (Prism, Highlight.js, EnlighterJS) included in this article, specify which highlighter you use and the markup it outputs so the selectors and getCodeText() handling can be tailored precisely.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *