Code Block Click Copy

Published: (December 9, 2025 at 03:51 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

I’m a back‑end‑leaning full‑stack developer, and after a long day fixing JavaScript code I decided to add “copy to clipboard” buttons to the code blocks on my site. The first version used the ClipboardJS library, but I quickly realized I could achieve the same result with the native Clipboard API and far less overhead.

Original implementation with ClipboardJS

Adding a button to each code block

const containers = document.querySelectorAll(`.${containerClass}`);

containers.forEach(container => {
  const button = document.createElement('button');
  button.className = buttonClass;
  button.innerHTML = icons.faCopyRegular;
  container.prepend(button);
});

Creating the ClipboardJS instance

const clipboard = new ClipboardJS(`.${buttonClass}`, {
  target: function (trigger) {
    return trigger.nextElementSibling;
  }
});

Handling a successful copy

clipboard.on('success', (e) => {
  if (e.action === 'copy') {
    const originalIcon = e.trigger.innerHTML;
    e.trigger.innerHTML = icons.faCheck;

    setTimeout(() => {
      e.trigger.innerHTML = originalIcon;
      e.clearSelection();
    }, iconChangeTimeout);
  }
});

Handling an error

clipboard.on('error', (e) => {
  console.error('ClipboardJS Error:', e.action, e.trigger);

  const originalIcon = e.trigger.innerHTML;
  e.trigger.innerHTML = icons.faBomb; // e.g., a cross or “times” icon

  setTimeout(() => {
    e.trigger.innerHTML = originalIcon;
  }, iconChangeTimeout);
});

While functional, this approach required loading the entire ClipboardJS library (and its dependencies) just to copy a few lines of text.

Refactoring to the native Clipboard API

The native API works with promises, allowing a more concise implementation.

Adding the button (unchanged)

const containers = document.querySelectorAll(`.${containerClass}`);

containers.forEach(container => {
  const button = document.createElement('button');
  button.className = buttonClass;
  button.innerHTML = icons.faCopyRegular;
  container.prepend(button);
});

Using navigator.clipboard.writeText

containers.forEach(container => {
  const button = container.querySelector(`.${buttonClass}`);
  button.addEventListener('click', function () {
    const text = this.nextElementSibling.textContent;
    window.navigator.clipboard.writeText(text)
      .then(() => {
        const originalIcon = this.innerHTML;
        this.innerHTML = icons.faCheck;

        setTimeout(() => {
          this.innerHTML = originalIcon;
        }, iconChangeTimeout);
      })
      .catch((e) => {
        console.error('Error copying to clipboard:', e);

        const originalIcon = this.innerHTML;
        this.innerHTML = icons.faBomb;

        setTimeout(() => {
          this.innerHTML = originalIcon;
        }, iconChangeTimeout);
      });
  });
});

This version eliminates the external dependency and mirrors the original behavior: showing a check mark on success or a bomb icon on failure, then restoring the original copy icon after a short timeout.

Final implementation

// icons.js should export the required SVG strings, e.g.:
// export const faCopyRegular = '';
// export const faCheck = '';
// export const faBomb = '';
import * as icons from './icons.js';

function addCopyToClipboardButtons() {
  const iconChangeTimeout = 1300;
  const containers = document.querySelectorAll('.highlight');

  containers.forEach(container => {
    const button = document.createElement('button');
    button.className = 'copy-button';
    button.innerHTML = icons.faCopyRegular;
    container.prepend(button);

    button.addEventListener('click', function () {
      const originalIcon = this.innerHTML;
      const text = this.nextElementSibling.textContent;

      window.navigator.clipboard.writeText(text)
        .then(() => {
          this.innerHTML = icons.faCheck;
          setTimeout(() => {
            this.innerHTML = originalIcon;
          }, iconChangeTimeout);
        })
        .catch(() => {
          console.error('Error copying to clipboard');
          this.innerHTML = icons.faBomb;
          setTimeout(() => {
            this.innerHTML = originalIcon;
          }, iconChangeTimeout);
        });
    });
  });
}

export { addCopyToClipboardButtons };

The function:

  1. Finds all elements with the .highlight class (the code block containers).
  2. Prepends a button containing the copy icon.
  3. On click, reads the sibling code element’s text and writes it to the system clipboard via navigator.clipboard.
  4. Shows a check icon on success or a bomb icon on failure, then restores the original copy icon after iconChangeTimeout milliseconds.

Conclusion

Switching from ClipboardJS to the native Clipboard API reduces bundle size and removes unnecessary dependencies while preserving the user experience. The final script is lightweight, easy to maintain, and works in all modern browsers that support the Clipboard API.

Back to Blog

Related posts

Read more »