Clipboard
A Stimulus behavior that copies text to the clipboard — either a value you give it or the content of an element — with optional copied feedback.
<div data-controller="clipboard" class="">
<code data-clipboard-target="source" class="">composer require symfony/ux-toolkit</code>
<span class="">
<button type="button" data-action="clipboard#copy" class="">Copy</button>
<span data-clipboard-target="success" class="">Copied!</span>
</span>
</div>
Installation
php bin/console ux:install clipboard --kit common
Copy the following file(s) into your app:
import { Controller } from '@hotwired/stimulus';
/**
* @value source Text to copy to the clipboard; when set, it takes precedence over the `source` target.
* @value successDuration How long, in milliseconds, the success feedback stays active after a copy. Defaults to 2000.
* @target source Element to copy from when no `source` value is set: its form value (input, textarea, select) or, failing that, its text content.
* @target success Element revealed briefly after a successful copy, then hidden again. Hidden on connect.
* @target idle Element hidden while the `success` target is showing, then revealed again; pair it with `success` inside the button to swap the label.
* @css-class success Class(es) added to the controller element for the same window, as a CSS-only alternative to the `success`/`idle` targets.
* @action copy Copies the source to the clipboard, then dispatches a `clipboard:copied` event and flashes the success feedback.
*/
export default class extends Controller {
static targets = ['source', 'success', 'idle'];
static classes = ['success'];
static values = {
source: String,
successDuration: { type: Number, default: 2000 },
};
#timeout;
connect() {
if (this.hasSuccessTarget) {
this.successTarget.hidden = true;
}
}
async copy(event) {
event.preventDefault();
const text = this.#text;
await navigator.clipboard.writeText(text);
this.dispatch('copied', { detail: { text } });
this.#flashSuccess();
}
get #text() {
if (this.hasSourceValue) {
return this.sourceValue;
}
// Form controls expose their content as `value`; everything else falls back to text content.
return this.sourceTarget.value ?? this.sourceTarget.textContent;
}
#flashSuccess() {
if (!this.hasSuccessTarget && !this.hasSuccessClass) {
return;
}
clearTimeout(this.#timeout);
this.#toggleSuccess(true);
this.#timeout = setTimeout(() => this.#toggleSuccess(false), this.successDurationValue);
}
#toggleSuccess(active) {
if (this.hasSuccessTarget) {
this.successTarget.hidden = !active;
}
if (this.hasIdleTarget) {
this.idleTarget.hidden = active;
}
this.successClasses.forEach((className) => this.element.classList.toggle(className, active));
}
}
Usage
Add data-controller="clipboard", mark the element to copy from with a source target, then trigger clipboard#copy from a button:
<div data-controller="clipboard">
<code data-clipboard-target="source">Text to copy</code>
<button type="button" data-action="clipboard#copy">Copy</button>
</div>
To copy a fixed string instead of an element, set a source value (data-clipboard-source-value) — see Copy using a Value.
Every successful copy also dispatches a clipboard:copied event, with the copied text available as event.detail.text. Listen for it to drive your own feedback.
Examples
Copy a Form Element's Value
Form controls — inputs, textareas, selects — are copied from their value:
<div data-controller="clipboard" class="">
<input type="text" data-clipboard-target="source" value="hello@example.com" readonly class="">
<button type="button" data-action="clipboard#copy" class="">Copy email</button>
</div>
Copy an Element's Text
For non-form elements, the source target's text content is copied:
<div data-controller="clipboard" class="">
<p data-clipboard-target="source" class="">The quick brown fox jumps over the lazy dog.</p>
<button type="button" data-action="clipboard#copy" class="">Copy text</button>
</div>
Copy Code
A classic: a code block with its own copy button. Point the source target at the <pre> — its text content is copied verbatim, indentation and newlines included:
<div data-controller="clipboard" class="">
<pre data-clipboard-target="source" class=""><code>$response = new JsonResponse([
'status' => 'ok',
]);</code></pre>
<button type="button" data-action="clipboard#copy" class="">
<span data-clipboard-target="idle">Copy</span>
<span data-clipboard-target="success" hidden>Copied!</span>
</button>
</div>
Copy using a Value
Set a source value to copy a fixed string, independent of what's shown on screen. Here the field displays a masked key while the button copies the real one:
<div data-controller="clipboard" data-clipboard-source-value="a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" class="">
<input type="text" value="xxxxxxxxxxxxxxxxxxxx" readonly class="">
<button type="button" data-action="clipboard#copy" class="">Copy API Key</button>
</div>
Copied Feedback
Add a success target to reveal a confirmation after a successful copy. It's hidden again after successDuration milliseconds (defaults to 2000):
<div data-controller="clipboard" data-clipboard-success-duration-value="1500" class="">
<code data-clipboard-target="source" class="">symfony serve -d</code>
<span class="">
<button type="button" data-action="clipboard#copy" class="">Copy</button>
<span data-clipboard-target="success" class="">Copied to clipboard!</span>
</span>
</div>
Swap the Button Label
Put both an idle and a success label inside the button: the idle one is hidden while the success one shows, swapping "Copy" for "Copied!" on click.
<div data-controller="clipboard" class="">
<code data-clipboard-target="source" class="">git clone git@github.com:symfony/ux.git</code>
<button type="button" data-action="clipboard#copy" class="">
<span data-clipboard-target="idle">Copy</span>
<span data-clipboard-target="success" hidden>Copied!</span>
</button>
</div>
Animate the Feedback
The success target is toggled from display: none to visible on copy, and showing a hidden element restarts its CSS animations — so an animated success target replays its effect on every click. Give it a burst @keyframes and match successDuration to the animation's length:
<style>
@keyframes clipboard-burst {
from { transform: scale(.75); opacity: .75; }
to { transform: scale(2) rotate(20deg); opacity: 0; }
}
</style>
<div data-controller="clipboard" data-clipboard-success-duration-value="450" class="">
<code data-clipboard-target="source" class="">php bin/console cache:clear</code>
<button type="button" data-action="clipboard#copy" class="">
Copy
<span data-clipboard-target="success" class="" style="animation: clipboard-burst 450ms linear forwards"></span>
</button>
</div>
Feedback via a CSS Class
Prefer to drive feedback purely from CSS? Instead of success/idle targets, set a success class with data-clipboard-success-class. The controller adds it to its element for successDuration, so you can restyle anything beneath it — including the trigger itself:
Pass multiple space-separated classes if you like (data-clipboard-success-class="ring pulse") — they're all applied for the duration.
<style>.clipboard-copied .clipboard-btn { background-color: #16a34a; }</style>
<div data-controller="clipboard" data-clipboard-success-class="clipboard-copied" class="">
<code data-clipboard-target="source" class="">php bin/console about</code>
<button type="button" data-action="clipboard#copy" class="">Copy</button>
</div>
Feedback with a Tooltip
Pair with the tooltip recipe for a floating confirmation. With both controllers on the button, copying dispatches clipboard:copied right there, opening a manual, self-hiding tooltip above it — no success target or class needed:
<style>
.tooltip { position: absolute; top: 0; left: 0; width: max-content; border-radius: .25rem; background: #4c1d95; color: #fff; padding: .25rem .5rem; font-size: .75rem; font-weight: 500; pointer-events: none; z-index: 50; }
.tooltip-arrow { position: absolute; width: 8px; height: 8px; background: inherit; transform: rotate(45deg); }
</style>
<div class="">
<div class="">
<code class="">php bin/console debug:router</code>
<button type="button" data-controller="clipboard tooltip" data-clipboard-source-value="php bin/console debug:router" data-action="clipboard#copy clipboard:copied->tooltip#show" data-tooltip-content-value="Copied!" data-tooltip-trigger-value="manual" data-tooltip-auto-hide-value="2000" class="">Copy</button>
</div>
</div>
API Reference
data-controller="clipboard"
| Value | Type | Default |
|---|---|---|
data-clipboard-source-value Text to copy to the clipboard; when set, it takes precedence over the
source target. |
String |
- |
data-clipboard-success-duration-value How long, in milliseconds, the success feedback stays active after a copy. Defaults to 2000.
|
Number |
2000 |
| Target | Description |
|---|---|
source |
Element to copy from when no source value is set: its form value (input, textarea, select) or, failing that, its text content. |
success |
Element revealed briefly after a successful copy, then hidden again. Hidden on connect. |
idle |
Element hidden while the success target is showing, then revealed again; pair it with success inside the button to swap the label. |
| Class | Description |
|---|---|
data-clipboard-success-class |
Class(es) added to the controller element for the same window, as a CSS-only alternative to the success/idle targets. |
| Action | Description |
|---|---|
copy |
Copies the source to the clipboard, then dispatches a clipboard:copied event and flashes the success feedback. |