Actions
Copy Button code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/copy-button.blade.php
@props([
'value' => null,
'target' => null,
'label' => 'Copy',
'copiedLabel' => 'Copied',
'variant' => 'outline',
'size' => 'base',
'icon' => 'copy',
'copiedIcon' => 'check',
'iconOnly' => false,
'duration' => 1800,
])
@php
/*
| One-click copy-to-clipboard button with a transient "Copied" confirmation.
| Copies the literal `value`, or — when `target` is a CSS selector — the value
| (inputs/textareas) or text content of the matched element. Falls back to a
| hidden textarea + execCommand where the async Clipboard API is unavailable.
|
| variant: outline, ghost, subtle, primary
| size: sm, base, lg
|
| Usage:
| <x-ui.copy-button value="npm install harbour" />
| <x-ui.copy-button target="#api-key" label="Copy key" variant="subtle" />
| <x-ui.copy-button value="hello" icon-only aria-label="Copy greeting" />
*/
$variants = [
'outline' => 'border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800',
'ghost' => 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-200',
'subtle' => 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700',
'primary' => 'bg-brand-600 text-white hover:bg-brand-700 dark:bg-brand-500 dark:hover:bg-brand-400',
];
$sizes = [
'sm' => 'text-xs gap-1 '.($iconOnly ? 'p-1.5' : 'px-2 py-1'),
'base' => 'text-sm gap-1.5 '.($iconOnly ? 'p-2' : 'px-2.5 py-1.5'),
'lg' => 'text-sm gap-2 '.($iconOnly ? 'p-2.5' : 'px-3 py-2'),
];
$iconSize = $size === 'sm' ? 'sm' : 'base';
$classes = implode(' ', [
'group inline-flex items-center justify-center rounded-lg font-medium transition cursor-pointer select-none',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/40',
$variants[$variant] ?? $variants['outline'],
$sizes[$size] ?? $sizes['base'],
]);
$accessibleLabel = $attributes->get('aria-label') ?: ($iconOnly ? $label : null);
@endphp
<button
type="button"
x-data="{
copied: false,
timer: null,
resolveText() {
@if ($value !== null)
return @js((string) $value);
@endif
@if ($target)
const el = document.querySelector(@js($target));
if (el) {
const fieldValue = el.value;
return (typeof fieldValue === 'string' && fieldValue !== '') ? fieldValue : (el.innerText ?? el.textContent ?? '');
}
@endif
return '';
},
async copy() {
const text = (this.resolveText() ?? '').toString();
try {
await navigator.clipboard.writeText(text.trim());
} catch (error) {
const area = document.createElement('textarea');
area.value = text.trim();
area.setAttribute('readonly', '');
area.style.position = 'absolute';
area.style.left = '-9999px';
document.body.appendChild(area);
area.select();
document.execCommand('copy');
area.remove();
}
this.copied = true;
this.$dispatch('copied', { text: text.trim() });
clearTimeout(this.timer);
this.timer = setTimeout(() => { this.copied = false; }, @js((int) $duration));
},
}"
@click="copy()"
:aria-label="copied ? @js($copiedLabel) : @js($accessibleLabel ?? $label)"
{{ $attributes->except('aria-label')->merge(['class' => $classes]) }}
>
<span class="relative inline-flex h-[1.25em] w-[1.25em] shrink-0 items-center justify-center" aria-hidden="true">
<x-ui.icon x-show="! copied" :name="$icon" :size="$iconSize" class="absolute" />
<x-ui.icon x-show="copied" x-cloak :name="$copiedIcon" weight="bold" :size="$iconSize" class="absolute text-green-600 dark:text-green-400" />
</span>
@unless ($iconOnly)
<span x-text="copied ? @js($copiedLabel) : @js($slot->isEmpty() ? $label : trim($slot))"></span>
@endunless
<span class="sr-only" role="status" aria-live="polite" x-text="copied ? @js($copiedLabel) : ''"></span>
</button>