mrshanebarron/copy-button
mrshanebarron/copy-button is a Laravel Blade package for adding one-click copy-to-clipboard buttons to your views. Use it to let users copy text, links, or code snippets with minimal setup and adjustable styling, while avoiding custom JavaScript for common clipboard actions.
The package ships a Blade component that renders a button wired to navigator.clipboard.writeText(). There is no runtime API to learn — the whole surface is "render the component, tell it what to copy."
Install:
composer require mrshanebarron/copy-button
Then check whether anything needs publishing (config, views, assets). Run php artisan vendor:publish with no arguments to see the available tags, or inspect the provider:
ls vendor/mrshanebarron/copy-button/src
cat vendor/mrshanebarron/copy-button/src/*ServiceProvider.php
If the button has styling or JS in a published asset, publish it and commit the result so the whole team shares one copy:
php artisan vendor:publish --tag=copy-button-assets # confirm the real tag first
php artisan view:clear
First use case — a copyable install command on a docs/onboarding page:
<pre id="install-cmd">composer require mrshanebarron/copy-button</pre>
<x-copy-button target="#install-cmd" label="Copy command" />
or, when you already have the value in PHP and don't want a DOM lookup:
<x-copy-button :text="$project->api_key" label="Copy API key" />
Where to look first, in this order:
src/View/Components/CopyButton.php — the constructor is the actual prop list (text vs target, label, maybe copiedLabel). Do not trust guessed prop names.resources/views/components/...) — tells you whether $attributes (and therefore class="...") is forwarded to the <button>.resources/views/components (anonymous). Anonymous components need nothing; class components need the provider auto-discovered.Two integration modes, chosen by data locality. Use text when the value comes from PHP (a token, a model field, a generated URL). Use target when the value already exists in the DOM (code samples, table cells) — the component reads it at click time, so it stays correct when Livewire/Alpine re-renders the source.
Copying real code blocks. Indentation inside a Blade template ends up inside the copied string. Keep snippets in a variable or partial and trim rather than inlining them in the markup:
@php
$snippet = <<<'SH'
php artisan migrate --seed
SH;
@endphp
<pre id="cmd">{{ $snippet }}</pre>
<x-copy-button target="#cmd" label="Copy" />
Inside loops (order numbers, IDs). Unique targets only:
@foreach ($orders as $order)
<span id="order-{{ $order->id }}">{{ $order->number }}</span>
<x-copy-button target="#order-{{ $order->id }}" label="Copy" />
@endforeach
Wrap it once, use it everywhere. Given the package is a one-person, zero-star dependency, put it behind your own component so you can swap it later without touching 40 views:
{{-- resources/views/components/ui/copy.blade.php --}}
<x-copy-button
:text="$text ?? null"
:target="$target ?? null"
label="{{ $label ?? 'Copy' }}"
{{ $attributes->class(['btn btn-sm']) }}
/>
Passing structured/awkward values. Bind, don't interpolate, so quotes and & survive:
<x-copy-button :text="$payload" /> {{-- fine --}}
<x-copy-button :text="json_encode($payload)" /> {{-- not {{ json_encode() }} --}}
Styling. If $attributes is merged onto the button, class="..." from the call site works. Otherwise publish the component view and edit it — never edit vendor/.
Testing. Feature tests can only assert markup; the copy itself is browser behaviour:
$this->get('/settings')
->assertSee('Copy API key')
->assertSee('data-target="#install-cmd"', false); // or however the view emits it
For the real behaviour, use Dusk and stub the clipboard so you're not fighting permissions in CI:
$this->browse(fn ($browser) => $browser
->script('window.navigator.clipboard = { writeText: (t) => { window.__copied = t; } };')
->click('@copy-install')
->script('return window.__copied;'));
In Livewire / Turbo / Inertia pages. If the package binds handlers on DOMContentLoaded, they won't rebind after a soft navigation. Either re-init on the framework's event or, better, delegate the listener at document level in your own small script.
navigator.clipboard is undefined on plain http:// other than localhost. Testing on http://192.168.1.x on a phone will look like "the button does nothing." Serve over HTTPS or use a tunnel.writeText() must run inside the click handler (transient activation). If you fetch the value first and copy in the .then(), Safari — and increasingly Chrome — rejects it. Copy synchronously from already-rendered text.document.execCommand), a display: none target copies an empty string. Use a visually-hidden technique instead of display:none if the element must be invisible.copy-button → CopyButton.php). That matters on case-sensitive filesystems: if a stale differently-cased file is left behind after upgrading, <x-copy-button> can fail to resolve on Linux while working fine on macOS. Run your suite on a Linux runner and delete the old file; if you overrode the component, match the new casing.php artisan view:clear. After config changes: php artisan config:clear. In production, php artisan optimize:clear — a cached compiled view is the usual reason "my edit did nothing."--force after upgrades, or include the package asset from vendor/ directly.onclick or an inline <script>, Content-Security-Policy without 'unsafe-inline' kills it silently in the console. Check the emitted markup; move the logic to a published file with a nonce if needed.aria-live="polite" feedback and keep a visible focus ring, or screen-reader users get no confirmation anything happened.ids from repeated components. Tooltips/labels keyed by id collide when you render many buttons; scope them or use $loop->index/$order->id."mrshanebarron/copy-button": "^1.0"), keep the wrapper component so replacement is one file, and re-run a Dusk/manual check after any composer update. The extension points worth knowing: whether the component view is publishable, whether config controls the default label/CSS, and whether the JS is separable — if none are, your wrapper is the extension point.How can I help you explore Laravel packages today?