Add click-to-copy JavaScript functionality to any WordPress site. Use it with any element (or WordPress block) just by setting a CSS class name. It’s ultra-lightweight and should work with page builders too.
Project requirements
- The code needs to work with any WordPress project just by adding it to a child theme – no complicated configuration
- Use native JavaScript so we don’t introduce any external dependencies (such as jQuery)
- Built-in styling, but it can be custom-styled on a per-project basis
- Usually we’ll want to click-to-copy an element’s on-screen text, but we might also want to click on an icon and copy some text
- Use CSS classes to enable click-to-copy, so the code should work with the WordPress Block Editor, Elementor and other page builders
We’re going to make this
Some interesting text
More interesting text
click the buttons to copy the text
Examples
Try clicking on the examples and pasting the result into a text editor…
Example one
lorem ipsum dolor sit amet
Click on this
Add “click-to-copy” to a WordPress Paragraph Block to enable basic click-to-copy. There’s no CSS styling, so you can make it look how ever you want.
Example two
lorem ipsum dolor sit amet
Click on this
Add the “fancy-click-to-copy” class to a Paragraph Block to apply some layout that should look familiar to users.
<a data-ctc="Stop" class="click-to-copy"> <i class="fas fa-stop"></i> </a> <a data-ctc="Play" class="click-to-copy"> <i class="fas fa-play"></i> </a> <a data-ctc="Pause" class="click-to-copy"> <i class="fas fa-pause"></i> </a>
Example three
Click on these
Put your “copy text” into the data-ctc="..."
attribute to specify what text should be copied when clicked. Useful when the clickable element is an icon, so the element’s innerText
property is empty. You still need to add “click-to-copy” to the element’s classes.
How it’s going to work
The core functionality will be implemented in native JavaScript, with logic like this:
When the document has loaded
- Scan the DOM for all elements that have a class of “click-to-copy” or “fancy-click-to-copy”
- For each element we find…
- If the element does not have the “click-to-copy” class, add it now
- If the element’s
data-ctc="..."
attribute is not set, then…- Create a
data-ctc="..."
attribute and copy the element’sinnerText
into the attribute
- Create a
- Attach a “click” event handler to the element
The click event handler
- The
target
of the click event might not be our click-to-copy element (it could be a child element). So we need to find the “closest” element in the DOM that has the “click-to-copy” CSS class and store this in a variable calledcontainer
- If
container
already has a tooltip then…- Don’t do anything
- Else if the element’s
data-ctc="..."
attribute is empty (i.e. there is no text to copy) then…- Don’t do anything
- Else…
- Copy the contents of
data-ctc="..."
to the clipboard - Create a small tooltip element (
div
) and add it tocontainer
as a child element - Set a timeout to remove the tooltip after 1.5 seconds (1,500 milliseconds) – configurable
- Copy the contents of
Featured plugin
Let’s write the code
In your WordPress child theme, create three empty files called “wpt-click-to-copy.php”, “wpt-click-to-copy.css” and “wpt-click-to-copy.js”. Open wpt-click-to-copy.php and paste the following into it:
<?php /** * Headwall WP Tutorials Click-to-Copy : WPTCTC * * https://wp-tutorials.tech/refine-wordpress/reusable-javascript-click-to-copy/ * * Changelog * * 2023-12-19 : Added the 'wpctc_is_required' filter to make it easier to * control which pages should include the click-to-copy CSS & JS. */ // Block direct access defined('WPINC') || die(); const WPTCTC_TOOLTIP_TIMEOUT = 1500; // milliseconds /** * Control what public-facing content should include our click-to-copy code. * By default, we want it on all pages/posts/products but not the front page. * If you want access to click-to-copy on EVERY page on your site, * return true from here. * * @return bool Whether or not to include click-to-copy frontend assets. */ function wptctc_is_required() { $is_required = is_singular() && !is_front_page(); return (bool) apply_filters('wpctc_is_required', $is_required); } /** * Maybe enqueue our JS & CSS assets, and configure our * global JS object, wptClickToCopy */ function wptctc_enqueue_scripts() { if (wptctc_is_required()) { $theme_version = wp_get_theme()->get('Version'); $base_uri = get_stylesheet_directory_uri(); $handle = 'wptcsc'; wp_enqueue_style( $handle, $base_uri . '/wpt-click-to-copy.css', null, // No style dependencies $theme_version ); wp_enqueue_script( $handle, $base_uri . '/wpt-click-to-copy.js', null, // No script dependencies $theme_version ); // Inject a script element that creates a global JavaScript object called // "wptClickToCopy" with some useful parameters for our JS code. wp_localize_script($handle, 'wptClickToCopy', [ 'tipCopied' => __('Copied', 'wp-tutorials'), 'tipTimeout' => WPTCTC_TOOLTIP_TIMEOUT, ]); // If you want to enqueue some of your own styles after the click-to-copy // assets have been enqueued, you can hook this action in your child theme. do_action('wptctc_enqueued_assets'); } } add_action('wp_enqueue_scripts', 'wptctc_enqueue_scripts');
The PHP code is a standard structure for mini JavaScript projects like this. All it needs to do is decide when to include our CSS & JS assets, and pass some parameters to the JS code using wp_localize_script().
To pull the code into your site, open your child theme’s functions.php and add the following couple of lines:
// Headwall WP Tutorials : Click-to-copy require_once dirname(__FILE__) . '/wpt-click-to-copy.php';
Next, open wpt-click-to-copy.css and add the following style definitions:
/** * Headwall WP Tutorials Click-to-Copy : WPTCTC * * https://wp-tutorials.tech/refine-wordpress/reusable-javascript-click-to-copy/ * */ .click-to-copy { cursor: pointer; position: relative; } /** * Tool-tip style */ .click-to-copy .ctc-tip { cursor: default; pointer-events: none; position: absolute; background-color: #fff; border: 2px solid lightgrey; font-size: 80%; border-radius: 0.5em; width: auto; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 0.5em 1em; line-height: 1em; text-align: center; font-style: italic; box-shadow: 0 0 1em #8888; white-space: nowrap; z-index: 10; color: black; } /** * The container for our "fancy" click-to-copy box. */ .fancy-click-to-copy { background-color: white; border-radius: 10px; overflow: hidden; border: 1px solid lightgrey; text-align: center; padding: 0 1em 0 2em; line-height: 2.5em; white-space: nowrap; } /** * Use Font Awesome 5 to add the icon to the left of the box. * If your site does not have FA5, you should change the font-family * to the equivalent that works on your site. */ .fancy-click-to-copy::before { font-family: 'Font Awesome 5 Free'; font-weight: 900; content: '\f0c5'; display: inline-block; position: absolute; left: 0; top: 50%; transform: translateY(-50%); transition: 0.3s color; border-right: 1px dotted darkgrey; padding: 0 1em; background-color: #eee; color: #666; z-index: 10; } .fancy-click-to-copy:hover::before { color: #3b0081; } /** * Useful when adding fancy-click-to-copy to a "code" block. */ .wp-block-code.fancy-click-to-copy code { display: unset; font-family: 'monospace'; overflow-wrap: unset; white-space: unset; }
The tool tip style is controlled by .click-to-copy .ctc-tip
near the top. Everything else is for the fancy box, so you can change it to suit your needs. In particular, the above makes reference to Font Awesome 5 for the icon. This should work fine on most projects, but you might need to modify the CSS if the icon doesn’t work for you.
The core JavaScript
Finally, we need to add the core logic to wpt-click-to-copy.js:
/** * Headwall WP Tutorials Click-to-Copy : WPTCTC * * https://wp-tutorials.tech/refine-wordpress/reusable-javascript-click-to-copy/ * * Changelog * * 2023-12-19 : Added support for a "copy-html" CSS class, so the element's * innerHTML is used to puplate dataset.ctc (instead of innerText). * To copy HTML, add "click-to-copy copy-html" or * "fancy-click-to-copy copy-html" CSS classes to your element. * * Added support for legacy copy-text when navigvator.clipboard is * not available. This is useful as a fallback when running the code * over "http" instead of "https". */ document.addEventListener('DOMContentLoaded', function() { 'use strict'; // Diagnostics // console.log('WPT Click-to-copy : load'); // Only proceed if wptClickToCopy has been specified as a // global JS variable. if (typeof wptClickToCopy !== 'undefined') { /** * Find all elements with either "click-to-copy" or "fancy-click-to-copy" * class and configure them. */ wptClickToCopy.init = () => { // Diagnostics // console.log('WPT Click-to-copy : init'); document.querySelectorAll('.click-to-copy, .fancy-click-to-copy').forEach(function(container) { // Make sure the element has "click-to-copy" as one of its classes, // as it might only have fancy-click-to-copy. if (!container.classList.contains('click-to-copy')) { container.classList.add('click-to-copy'); } // The actual text being copied is in the data-ctc="..." attribute. // If this hasn't been set, copy the element's inner text (or inner // HTML) into the data attribute now. if (typeof container.dataset.ctc !== 'undefined') { // the click-to-copy text has already been specified, so we don't // need to do anything. } else if (container.classList.contains('copy-html')) { container.dataset.ctc = container.innerHTML; } else { container.dataset.ctc = container.innerText; } // Add the element's click event handler. container.addEventListener('click', wptClickToCopy.clickHandler); }); }; /** * Handle click events on our click-to=-copy elements. * * @param object event JavaScript event */ wptClickToCopy.clickHandler = (event) => { event.preventDefault(); // The user might have clicked on a child of the click-to-copy element // (or on the tool tip) sowe need to use .closest() to recurse up the // DOM and find the correct element. e.g. this could be event.target, // event.target.parentElement or even event.target.parentElement.parentElement let container = event.target.closest('.click-to-copy'); let copyText = ''; if (!container) { // The clicked element doesn't have the "click-to-copy" class. } else if (wptClickToCopy.hasTip(container)) { // The container already has a tip showing. } else if ((copyText = container.dataset.ctc).length == 0) { // There's no text to copy (empty string) } else { // If the site is using HTTPS then we can use navigator.clipboard to // copy the text. Otherwise we need to create a temporary textarea // element, populate it and call document.execCommand('copy')... // it's a bit of a fiddle, but it should work in older browsers and // with non-https connections. if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(copyText); } else if (typeof document.execCommand === 'function') { const textArea = document.createElement('textarea'); textArea.value = copyText; textArea.style.position = 'absolute'; textArea.style.left = '-100px'; textArea.style.top = '0px'; textArea.style.width = '50px'; document.body.prepend(textArea); try { textArea.focus(); textArea.select(); document.execCommand('copy'); } catch (error) { console.error(error); } finally { textArea.remove(); } } else { console.error('Unable to copy the text'); } // Add a temporary tooltip to the click-to-copy element, using the // contents of wptClickToCopy.tipCopied as the label (passed from // the back-end PHP). wptClickToCopy.addTip(container, wptClickToCopy.tipCopied); } }; /** * Utility function that tells us if an element already has a tooltip, * or not. * * @param object container The click-to-copy element. * @return bool Does this element have a child tooltip */ wptClickToCopy.hasTip = (container) => { return container.querySelectorAll('.ctc-tip').length > 0; }; /** * Remove all child tool tips from this element. * * @param object container The click-to-copy element. */ wptClickToCopy.removeTip = (container) => { container.querySelectorAll('.ctc-tip').forEach((tip) => { // console.log(`Remove: ${tip.innerText}`); tip.remove(); }); }; /** * Add a tool tip to this element. * * @param object container The click-to-copy element. * @param string tipText The text to show in the tool tip */ wptClickToCopy.addTip = (container, tipText) => { const tip = document.createElement('div'); tip.classList.add('ctc-tip'); tip.append(tipText); container.append(tip); // After the number of milliseconds specified in "wptClickToCopy.tipTimeout", // remove the tool tip form the DOM. if (wptClickToCopy.tipTimeout > 0) { setTimeout(() => { tip.remove(); }, wptClickToCopy.tipTimeout); } }; /** * Main entry point. */ wptClickToCopy.init(); } });
If you were to take out the comments, there wouldn’t be much code left in here. It was tempting use jQuery to create animated tooltips (or something like the Popper JS library). But I think it’s better to keep things really lean here because the code can end up on lots of pages… and we want to minimise impact on page-load speeds.
Testing
Make sure everything is saved and try adding “fancy-click-to-copy” to a paragraph block that’s got some text in it. Save the content and check it out in the front-end.
If it doesn’t seem to work, edit the JavaScript, uncomment the Diagnostics lines and enable your browser’s Dev Tools… then reload the page.
// Diagnostics console.log('WPT Click-to-copy : load');
On reload, you should see WPT Click-to-copy : load
in the output. If it’s not there your assets aren’t being enqueued, so you should check the return value of wptctc_is_required()
in “wpt-click-to-copy.php”. Try setting it to always return true
so the assets are enqueued on every page.
Have fun clicking and copying ๐ ๐
This came in very handy, thank you! I modified slightly so I could copy the HTML rather than just the text โย instead of:
if ((copyText = container.dataset.ctc).length == 0)
I wrote
if ((copyText = container.innerHTML).length == 0)
Have not tested extensively yet to be sure this will work, but I suspect it should.
I’ve had a look through the code again and I think we need to keep using container.dataset.ctc, but I’ve put your suggestion into the JS init() function. So, if the container element has got the “copy-html” CSS class then we use innerHTML to populate dataset.ctc, otherwise we use innerText to populate dataset.ctc as the fallback. I’ve also put some code in there to fallback to an alternative copy-text method if the navigator.clipboard object isn’t available.
To use it, just use “click-to-copy copy-html” as your element’s CSS class ๐