Add a Copy to Clipboard Button to WordPress Code Blocks

Readers of a WordPress tutorial or documentation site copy code all the time, and selecting a long block by hand often misses the first or last line. This snippet adds a Copy button to every code block on your WordPress site. One click puts the whole block on the clipboard, and the button says Copied!.

It is plain CSS and JavaScript, with no plugin, jQuery or library. It works with the core Code block and with any <pre> your theme or a page builder prints.

The CSS

/* Copy button on code blocks (dpc). */
.dpc-copy {
	position: relative;
}

.dpc-copy-btn {
	position: absolute;
	top: 0.5em;
	right: 0.5em;
	padding: 0.25em 0.6em;
	border: 1px solid currentColor;
	border-radius: 4px;
	background: rgba(255, 255, 255, 0.9);
	color: #1e1e1e;
	font: 600 12px/1.4 system-ui, sans-serif;
	cursor: pointer;
	opacity: 0;
	transition: opacity 0.15s;
}

.dpc-copy:hover .dpc-copy-btn,
.dpc-copy-btn:focus-visible,
.dpc-copy-btn.is-copied {
	opacity: 1;
}

@media (hover: none) {
	.dpc-copy-btn {
		opacity: 1;
	}
}

The JavaScript

(function () {
	var LABEL = 'Copy';
	var COPIED = 'Copied!';
	var FAILED = 'Press Ctrl+C';

	function copyText(text) {
		if (navigator.clipboard && window.isSecureContext) {
			return navigator.clipboard.writeText(text);
		}
		// Fallback for plain http:// sites, where the Clipboard API is not available.
		return new Promise(function (resolve, reject) {
			var area = document.createElement('textarea');
			area.value = text;
			area.setAttribute('readonly', '');
			area.style.position = 'fixed';
			area.style.opacity = '0';
			document.body.appendChild(area);
			area.select();
			var ok = false;
			try {
				ok = document.execCommand('copy');
			} catch (e) {
				ok = false;
			}
			document.body.removeChild(area);
			if (ok) {
				resolve();
			} else {
				reject(new Error('copy failed'));
			}
		});
	}

	function addButton(pre) {
		if (pre.classList.contains('dpc-copy')) {
			return;
		}
		var button = document.createElement('button');
		button.type = 'button';
		button.className = 'dpc-copy-btn';
		button.textContent = LABEL;
		button.setAttribute('aria-live', 'polite');

		button.addEventListener('click', function () {
			var code = pre.querySelector('code');
			var text = code ? code.textContent : Array.prototype.filter.call(pre.childNodes, function (node) {
				return node !== button;
			}).map(function (node) {
				return node.textContent;
			}).join('');

			copyText(text).then(function () {
				button.textContent = COPIED;
			}, function () {
				button.textContent = FAILED;
			}).then(function () {
				button.classList.add('is-copied');
				setTimeout(function () {
					button.textContent = LABEL;
					button.classList.remove('is-copied');
				}, 2000);
			});
		});

		pre.classList.add('dpc-copy');
		pre.appendChild(button);
	}

	function init() {
		document.querySelectorAll('pre').forEach(addButton);
	}

	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}
})();

Add it with Scripts Organizer

In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Conditions and leave Template on All. Under Script location tick Header and Footer.

  • Header: set Header Mode to CSS and paste the CSS.
  • Footer: set Footer Mode to Javascript and paste the JavaScript.

Publish and enable the block. Scripts Organizer prints Header and Footer code on the front end only with the Conditions trigger, not with Everywhere. To load it only on your blog, narrow the Template condition to posts.

How it works

  • On page load the script finds every <pre>, adds the class dpc-copy and appends a <button>. The class also stops it from adding a second button.
  • The copied text is the textContent of the <code> inside the <pre>, or of the <pre> itself without the button. Syntax highlighters that wrap tokens in <span>s still give the plain code.
  • navigator.clipboard.writeText() does the copying. It only exists on HTTPS (and localhost), so on plain http:// the script falls back to a hidden textarea and document.execCommand('copy').
  • The button is a real <button>: keyboard users can tab to it, and aria-live makes screen readers announce Copied!. It shows on hover, on keyboard focus, and always on touch screens.
  • The older version of this post, based on WPJohnny’s Code Click-to-Copy plugin, copied on a click anywhere in the code, including inline <code> in paragraphs, so readers could no longer select part of a block. It also used the deprecated global event and an Oxygen colour variable.

Customise it

  • Other labels or language: change LABEL, COPIED and FAILED at the top of the script.
  • Only core Code blocks: change querySelectorAll('pre') to querySelectorAll('pre.wp-block-code').
  • Colours and position: edit .dpc-copy-btn in the CSS. Use your theme’s colour variables there if it has them.

Gotchas

  • On a code block that scrolls sideways, the button sits at the top right of the visible box and moves with the content when it scrolls. Wrap long lines with white-space: pre-wrap if that bothers you.
  • Code added later by JavaScript (tabs, AJAX loading) gets no button. Add window.dpcCopyInit = init; above the readyState check and call dpcCopyInit() after the new content is in the page.
  • The script does not load in the block editor; there it would get in the way of editing.

In the Scripts Organizer library Import it with one click from Scripts Organizer.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.