Skip to content

Instantly share code, notes, and snippets.

@Potherca
Last active June 14, 2024 15:41
Show Gist options
  • Save Potherca/33b4d10024f56ba0610f8e70477687cb to your computer and use it in GitHub Desktop.
Save Potherca/33b4d10024f56ba0610f8e70477687cb to your computer and use it in GitHub Desktop.

Copy Button

This gist contains the recipe to make a copy button.

It is activated by adding data-js="copy" to an element and including the CSS and JS files.

<link rel="stylesheet" href="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.css">
      <script async src="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.js"></script>
<pre data-js="copy">Your Text Goes Here</pre>

The button itself is an SVG that is added to an HTML element using an ::after pseudo-element.

The JS is an EventListener on a Click event, and uses navigator.clipboard to copy the text.

Once the text is copied, the button is given a CSS class that changes the SVG to a checkmark.

Both SVG icons are taken from Material Line Icons.

It can be seen in action at https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb

If the HTML of the target should be copied (rather than the text) use:data-js="copy copy-html". This will also copy any HTML tags present in the content.

To have a button that copies something else, add data-js-copy="selector", where selector can be any valid CSS selector.

For example:

<link rel="stylesheet" href="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.css">
<script async src="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.js"></script>
<a data-js="copy" data-js-copy="pre[data-js-copy-this='target']" title="Copy the text below"></a>
<pre data-js-copy-this="target">Your Text Goes Here</pre>

screenshot of the page

/* <textarea> does not support an `::after` pseudo-element so some selector wrangling is needed */
[data-js~="copy"]:not(textarea),
:has(> textarea[data-js~="copy"]) {
padding-right: 2em;
position: relative;
transition: all 1s ease-in-out;
}
[data-js~="copy"]:not(textarea)::after,
:has(> textarea[data-js~="copy"])::after {
/* https://icon-sets.iconify.design/line-md/clipboard-arrow/ */
content: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" viewBox="0 0 24 24"%3E%3Cg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"%3E%3Cg stroke-width="2"%3E%3Cpath stroke-dasharray="16" stroke-dashoffset="16" d="M12 3H19V11"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="16%3B0"%2F%3E%3C%2Fpath%3E%3Cpath stroke-dasharray="44" stroke-dashoffset="44" d="M19 17V21H5V3H12"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" begin="0.2s" dur="0.4s" values="44%3B0"%2F%3E%3C%2Fpath%3E%3Cpath stroke-dasharray="10" stroke-dashoffset="10" d="M21 14H12.5"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" begin="1s" dur="0.2s" values="10%3B0"%2F%3E%3C%2Fpath%3E%3Cpath stroke-dasharray="6" stroke-dashoffset="6" d="M12 14L15 17M12 14L15 11"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" begin="1.2s" dur="0.2s" values="6%3B0"%2F%3E%3C%2Fpath%3E%3C%2Fg%3E%3Cpath stroke-dasharray="12" stroke-dashoffset="12" d="M14.5 3.5V6.5H9.5V3.5"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" begin="0.7s" dur="0.2s" values="12%3B0"%2F%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
backdrop-filter: blur(10px);
cursor: pointer;
opacity: 0.35;
position: absolute;
right: 0.15em;
top: 0.15em;
width: 1.35em;
}
[data-js~="copy"]:not(textarea):hover::after,
:has(> textarea[data-js~="copy"]):hover::after {
opacity: 1;
}
[data-js~="copy"]:not(textarea).copied::after,
:has(> textarea[data-js~="copy"]).copied::after {
/* https://icon-sets.iconify.design/line-md/clipboard-check-twotone/ */
content: url('data:image/svg+xml,%3Csvg xmlns="http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg" viewBox="0 0 24 24"%3E%3Cpath fill="lime" fill-opacity="0" d="M6 4H10V6H14V4H18V20H6V4Z"%3E%3Canimate fill="freeze" attributeName="fill-opacity" begin="1.2s" dur="0.15s" values="0%3B0.3"%2F%3E%3C%2Fpath%3E%3Cg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"%3E%3Cg stroke-width="2"%3E%3Cpath stroke-dasharray="66" stroke-dashoffset="66" d="M12 3H19V21H5V3H12Z"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="66%3B0"%2F%3E%3C%2Fpath%3E%3Cpath stroke-dasharray="10" stroke-dashoffset="10" d="M9 13L11 15L15 11"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" begin="1s" dur="0.2s" values="10%3B0"%2F%3E%3C%2Fpath%3E%3C%2Fg%3E%3Cpath stroke-dasharray="12" stroke-dashoffset="12" d="M14.5 3.5V6.5H9.5V3.5"%3E%3Canimate fill="freeze" attributeName="stroke-dashoffset" begin="0.7s" dur="0.2s" values="12%3B0"%2F%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E');
fill: lime;
opacity: 1;
}
:has(> textarea[data-js~="copy"]) {
display: block;
width: calc(100% + 2.05em);
}
document.addEventListener('readystatechange', (event) => {
let loaded = false
if (
loaded === false
&& (
event.target.readyState === 'interactive'
|| event.target.readyState === 'complete'
)
) {
document.querySelectorAll('[data-js~="copy"]').forEach(element => element.addEventListener('click', event => {
let target = event.currentTarget
let subject
if (
target.dataset.jsCopy !== undefined
&& document.querySelector(target.dataset.jsCopy) !== null
) {
let otherTarget = document.querySelector(target.dataset.jsCopy)
if (target.dataset.js.includes('copy-html')) {
target = otherTarget.innerHTML
} else {
target = otherTarget
}
}
if (target.dataset.js && target.dataset.js.includes('copy-html')) {
subject = target.innerHTML
} else {
if (target.tagName.toLowerCase() === 'textarea') {
subject = target.value
target = target.parentElement
} else {
subject = target.innerText
}
}
navigator.clipboard.writeText(subject.trim())
.then(() => {
event.target.classList.add('copied')
setTimeout(() => event.target.classList.remove('copied'), 2200)
})
.catch(e => console.error(e))
}))
loaded = true
}
})
<!doctype html>
<html lang="en">
<meta charset="UTF-8">
<title>Copy Button</title>
<link rel="icon" href="https://favicon.potherca.workers.dev/36" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<!-- HTML Meta Tags -->
<meta name="description" content="CSS + JS Copy Button">
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb">
<meta property="og:type" content="website">
<meta property="og:title" content="Copy Button">
<meta property="og:description" content="CSS + JS Copy Button">
<meta property="og:image" content="https://user-images.githubusercontent.com/195757/236698925-4969f801-61fb-4e16-81ae-0d8c8893a9d2.png">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="gist.pother.ca">
<meta property="twitter:url" content="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb">
<meta name="twitter:title" content="Copy Button">
<meta name="twitter:description" content="CSS + JS Copy Button">
<meta name="twitter:image" content="https://user-images.githubusercontent.com/195757/236698925-4969f801-61fb-4e16-81ae-0d8c8893a9d2.png">
<!-- Meta Tags Generated via https://www.opengraph.xyz -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/picnic">
<link rel="stylesheet" href="https://pother.ca/CssBase/css/created-by-potherca.css">
<link rel="stylesheet" href="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.css">
<script async src="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.js"></script>
<style>
article {
background-color: #fff;
border-bottom-left-radius: 0.5em;
border-bottom-right-radius: 0.5em;
}
body {
background-color: #def;
}
pre {
display: block;
margin: 0.5em;
padding: 0.5em;
white-space: pre-wrap;
}
form {
text-align: center;
}
h1 {
width: 10em
}
.created-by {
padding: 0.5em;
text-align: center;
width: 100%;
}
</style>
<article class="flex two-third-500 off-sixth-500">
<header class="full">
<h1 data-js="copy" data-js-copy="pre[data-js='copy']">Copy Button</h1>
</header>
<main class="full">
<p>
This page contains the recipe to make a copy button.
</p>
<p>
It is activated by adding <code>data-js="copy"</code> to an element and including the CSS and JS files.
</p>
<p>
The button itself is an SVG that is added to an HTML element using
<a href="https://developer.mozilla.org/docs/Web/CSS/::after">an <code>::after</code> pseudo-element</a>.
</p>
<p>
The JS is an
<a href="https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener">EventListener</a> on a
<a href="https://developer.mozilla.org/docs/Web/API/Element/click_event">Click event</a>, and uses
<a href="https://developer.mozilla.org/docs/Web/API/Navigator/clipboard"><code>navigator.clipboard</code></a> to copy the text.
</p>
<p>
Once the text is copied, the button is given a CSS class that changes the SVG to a checkmark.
</p>
<p>
Both SVG icons are taken from <a href="https://cyberalien.github.io/line-md/">Material Line Icons</a>.
</p>
<p>
It can be seen in action below.
</p>
<pre data-js="copy">
&lt;link rel="stylesheet" href="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.css"&gt;
&lt;script async src="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.js"&gt;&lt;/script&gt;
&lt;pre data-js="copy"&gt;Your Text Goes Here&lt;/pre&gt;
</pre>
<p data-js="copy copy-html">
If the <em>HTML</em> of the target should be copied (rather than the text) use:
<code>data-js="copy copy-html"</code>.
This will also copy any
<a href="https://developer.mozilla.org/docs/Web/HTML"><abbr>HTML</abbr> tags</a> present in the content.
</p>
<p>
To have a button that copies something else, add <code>data-js-copy="selector"</code>, where
<code>selector</code> can be any valid
<a href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors">CSS selector.</a>
</p>
<p>For example: <a data-js="copy" data-js-copy="pre[data-js-copy='target']" title="Copy the text below"></a></p>
<pre data-js-copy="target">
&lt;link rel="stylesheet" href="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.css"&gt;
&lt;script async src="https://gist.pother.ca/33b4d10024f56ba0610f8e70477687cb/copy-button.js"&gt;&lt;/script&gt;
&lt;a data-js=&quot;copy&quot; data-js-copy=&quot;pre[data-js-copy-this=&apos;target&apos;]&quot; title=&quot;Copy the text below&quot;&gt;&lt;/a&gt;
&lt;pre data-js-copy-this="target"&gt;Your Text Goes Here&lt;/pre&gt;
</pre>
</main>
</article>
<footer class="created-by">
<p>
Source of this page is
available on <a href="https://gist.github.com/Potherca/33b4d10024f56ba0610f8e70477687cb">Github</a>
under a <a rel="license" href="https://spdx.org/licenses/MPL-2.0.html"
>Mozilla Public License 2.0</a> &mdash; Created by <a href="https://pother.ca/" class="potherca">Potherca</a>
</p>
</footer>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment