Skip to main content

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>