Add a Copy Button to Every Code Block in WordPress

If you publish snippets in WordPress, you probably want a Copy button on every core Code block. Readers can then grab the code without dragging a selection across the lines and missing the last one. Core doesn’t include that button, and most plugins that add one also bring in a syntax highlighter you may not want.

This small plugin adds a Copy button to the top-right corner of every core Code block (pre.wp-block-code) on the front end. When clicked, the button copies the code, changes to Copied! for two seconds and then goes back to Copy. It uses vanilla JavaScript and the asynchronous Clipboard API, with no jQuery. The JS and CSS only load on pages that actually render a Code block. I assumed you want the button on the front end only, not in the editor.

The snippet

Create a folder wp-content/plugins/dpc-code-copy/ containing three files. The first one is the PHP loader, dpc-code-copy.php:

<?php
/**
 * Plugin Name: DPC Code Block Copy Button
 * Description: Adds a Copy button to every core Code block on the front end.
 * Version:     1.0.0
 */

defined( 'ABSPATH' ) || exit;

const DPC_CODE_COPY_VERSION = '1.0.0';

add_action( 'init', 'dpc_code_copy_register_assets' );
function dpc_code_copy_register_assets(): void {
	wp_register_script(
		'dpc-code-copy',
		plugins_url( 'copy.js', __FILE__ ),
		array(),
		DPC_CODE_COPY_VERSION,
		array(
			'in_footer' => true,
			'strategy'  => 'defer',
		)
	);

	wp_add_inline_script(
		'dpc-code-copy',
		'window.dpcCodeCopy = ' . wp_json_encode(
			array(
				'copy'   => __( 'Copy', 'dpc-code-copy' ),
				'copied' => __( 'Copied!', 'dpc-code-copy' ),
				'failed' => __( 'Copy failed', 'dpc-code-copy' ),
			)
		) . ';',
		'before'
	);

	wp_register_style(
		'dpc-code-copy',
		plugins_url( 'copy.css', __FILE__ ),
		array(),
		DPC_CODE_COPY_VERSION
	);
}

// Enqueue only when a core Code block is actually rendered.
add_filter( 'render_block_core/code', 'dpc_code_copy_enqueue_on_render' );
function dpc_code_copy_enqueue_on_render( string $block_content ): string {
	if ( ! is_admin() ) {
		wp_enqueue_script( 'dpc-code-copy' );
		wp_enqueue_style( 'dpc-code-copy' );
	}

	return $block_content;
}

Next, copy.js:

( () => {
	const labels = Object.assign(
		{ copy: 'Copy', copied: 'Copied!', failed: 'Copy failed' },
		window.dpcCodeCopy || {}
	);

	// The Clipboard API only exists on HTTPS (and localhost).
	if ( ! window.isSecureContext || ! navigator.clipboard ) {
		return;
	}

	document.querySelectorAll( 'pre.wp-block-code' ).forEach( ( pre ) => {
		const code = pre.querySelector( 'code' );

		if ( ! code || pre.querySelector( ':scope > .dpc-code-copy' ) ) {
			return;
		}

		const button = document.createElement( 'button' );
		button.type = 'button';
		button.className = 'dpc-code-copy';
		button.textContent = labels.copy;
		button.setAttribute( 'aria-live', 'polite' );

		let timer;

		button.addEventListener( 'click', async () => {
			try {
				await navigator.clipboard.writeText( code.innerText );
				button.textContent = labels.copied;
			} catch {
				button.textContent = labels.failed;
			}

			clearTimeout( timer );
			timer = setTimeout( () => {
				button.textContent = labels.copy;
			}, 2000 );
		} );

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

Finally, copy.css:

.wp-block-code.dpc-has-copy {
	position: relative;
}

.wp-block-code.dpc-has-copy > code {
	padding-right: 5rem;
}

.dpc-code-copy {
	position: absolute;
	top: 0.5rem;
	right: 0.5rem;
	padding: 0.25rem 0.6rem;
	font-family: system-ui, sans-serif;
	font-size: 0.75rem;
	line-height: 1.4;
	color: inherit;
	background: color-mix(in srgb, currentColor 10%, transparent);
	border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
	border-radius: 4px;
	cursor: pointer;
	opacity: 0.75;
}

.dpc-code-copy:hover,
.dpc-code-copy:focus-visible {
	opacity: 1;
}

Activate DPC Code Block Copy Button under Plugins. You can also drop all three files into wp-content/mu-plugins/, but then you need a loader file there, because WordPress doesn’t scan subfolders of mu-plugins.

How it works

  • The script and style are registered on init, not on wp_enqueue_scripts. A block theme renders its template before wp_head() runs, so the handles have to exist before any block renders.
  • render_block_core/code runs once for each Code block. Enqueuing there means pages without code load no extra files. In a block theme the stylesheet still prints in the <head>. The pattern is the same as in Load script only when you need it.
  • The labels go to JS through wp_json_encode() in a before inline script. They are translatable and safely encoded, and the defer strategy stays intact.
  • The script finds every pre.wp-block-code, adds one button to each, and copies code.innerText. innerText turns old-style <br> line breaks into real newlines.
  • One timer per button resets the label after 2000 ms. Clicking again during that time restarts the two seconds instead of stacking timers.
  • aria-live="polite" makes screen readers announce the change to “Copied!”.

Customise it

  • Change the wording in the __() calls, or translate it with the dpc-code-copy text domain.
  • Change 2000 in copy.js to change how long “Copied!” stays visible.
  • To show the button only on hover, set opacity: 0 on .dpc-code-copy and add .wp-block-code:hover .dpc-code-copy { opacity: 1; }. Keep the :focus-visible rule so keyboard users can still see it.
  • To restyle the button with your theme colours, swap currentColor for preset variables such as var(--wp--preset--color--contrast).

Gotchas

  • The Clipboard API only works over HTTPS or on localhost. On plain HTTP the script adds no buttons, so you never get a button that can’t do anything.
  • The core Code block wraps long lines by default. If your theme switches to white-space: pre with horizontal scrolling, the button scrolls along with the content because it sits inside the pre.
  • Buttons are only added to code that exists when the page loads. Code inserted later by AJAX or infinite scroll needs the loop wrapped in a function you call again.
  • An older post on this site, Copy code to clipboard, covers a general copy button. This one targets the core Code block and loads its files only when needed.

If you’d rather not create a plugin folder, Scripts Organizer lets you add the JS and CSS as snippets from inside WordPress.

// 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.