Skip to content
Brok UI

Loading…

No results

Copy Button

Open source

A ghost button that copies text to the clipboard and cross-fades copy → check (or a failed mark), announced to screen readers; icon-only by default, or `show-label` for Copy → Copied text.

Version
v1.1.2
Stability
stable
License
MIT
Related
Input Copy
Code Line
Code Block

Preview

composer require jml/brok
php artisan ui:add copy-button
previews.components.copy-button.default.blade.php Blade
{{-- Icon-only (the accessible name is `label`), and `show-label` for Copy → Copied text. --}}
<div class="flex w-full max-w-md flex-col gap-4">
    <div class="flex items-center gap-3 rounded-md border border-border bg-muted ps-4 pe-2 py-2 text-sm text-foreground">
        <span class="flex-1 truncate font-mono">composer require jml/brok</span>
        <x-ui.copy-button value="composer require jml/brok" label="Copy command" />
    </div>
    <div class="flex items-center gap-3 rounded-md border border-border bg-muted ps-4 pe-2 py-2 text-sm text-foreground">
        <span class="flex-1 truncate font-mono">php artisan ui:add copy-button</span>
        <x-ui.copy-button value="php artisan ui:add copy-button" show-label variant="outline" />
    </div>
</div>

Installation

terminal
php artisan ui:add copy-button

Note

This component ships an Alpine behavior module at resources/js/ui/copy-button.js. Import it once from your bundle so it registers on alpine:init:

resources/js/ui/index.js JS
import './copy-button.js';

Registry contract

php artisan ui:add copy-button writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/copy-button.blade.php
  • js resources/js/ui/copy-button.js
Registry dependencies
button
Packages
composer: jml/brok:^0.2
npm: alpinejs

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

copy-button.md
# Brok UI: Copy Button (`copy-button`)

A ghost button that copies text to the clipboard and cross-fades copy → check (or a failed mark), announced to screen readers; icon-only by default, or `show-label` for Copy → Copied text.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add copy-button
```

## Usage

```blade
{{-- Icon-only (the accessible name is `label`), and `show-label` for Copy → Copied text. --}}
<div class="flex w-full max-w-md flex-col gap-4">
    <div class="flex items-center gap-3 rounded-md border border-border bg-muted ps-4 pe-2 py-2 text-sm text-foreground">
        <span class="flex-1 truncate font-mono">composer require jml/brok</span>
        <x-ui.copy-button value="composer require jml/brok" label="Copy command" />
    </div>
    <div class="flex items-center gap-3 rounded-md border border-border bg-muted ps-4 pe-2 py-2 text-sm text-foreground">
        <span class="flex-1 truncate font-mono">php artisan ui:add copy-button</span>
        <x-ui.copy-button value="php artisan ui:add copy-button" show-label variant="outline" />
    </div>
</div>
```

## Props

- `value` (string, default ``) — The text written to the clipboard on click.
- `timeout` (int, default `2000`) — Milliseconds the copied (or failed) state stays shown before reverting to idle.
- `label` (string, default `Copy`) — Accessible name at rest (icon form), or visible text at rest when showLabel is true.
- `copiedLabel` (string, default `Copied`) — Announced by the live region, and shown in place of label in the labelled form, after a successful copy.
- `failedLabel` (string, default `Copy failed`) — Announced and shown when the clipboard write is refused.
- `showLabel` (bool, default `false`) — Shows the label text beside the icon instead of the icon-only form.
- `variant` (ghost|outline|secondary|default|link|destructive, default `ghost`) — Forwarded to the composed button; any button variant.
- `size` (sm|md|lg, default `sm`) — Forwarded to the composed button; any button size.
- `shape` (string, default `default`) — Forwarded to the composed button; any button shape.

## Use when

- Use for explicit actions and commits; keep one action visually primary when several sit together.
- Letting the user copy a code snippet, token or command to the clipboard in one click.
- Pairing a compact icon-only trigger beside a value the user is likely to paste elsewhere.

## Avoid when

- Do not use button treatment for navigation-only view changes when links, tabs, or radios describe state more clearly.
- The value is already editable and visible in a field with its own copy affordance; use input-copy instead.
- The action does more than write to the clipboard; use a plain button instead.

## Anti-patterns

- Using a button for navigation-only state changes
- Presenting multiple equally dominant primary actions

## Rules

- Use the `<brok:copy-button>` tag (or `<x-ui.copy-button>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/copy-button
- Registry JSON (files, props, contract): https://brokui.dev/r/open/copy-button.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
value string The text written to the clipboard on click.
timeout int 2000 Milliseconds the copied (or failed) state stays shown before reverting to idle.
label string Copy Accessible name at rest (icon form), or visible text at rest when showLabel is true.
copiedLabel string Copied Announced by the live region, and shown in place of label in the labelled form, after a successful copy.
failedLabel string Copy failed Announced and shown when the clipboard write is refused.
showLabel bool false Shows the label text beside the icon instead of the icon-only form.
variant ghost | outline | secondary | default | link | destructive ghost Forwarded to the composed button; any button variant.
size sm | md | lg sm Forwarded to the composed button; any button size.
shape string default Forwarded to the composed button; any button shape.

Slots

  • default — Unused; the icon and label are rendered from props.

Data slots

Stable hooks for CSS overrides and browser tests.

copy-button copy-button-label

Behavior

  • Writes value to the clipboard on click and cross-fades the icon from copy to a check mark.
  • Reverts to the idle icon automatically after timeout milliseconds.
  • Falls back to a failed state (and failedLabel) when the clipboard write is refused, such as in an insecure context or without permission.
  • Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
  • Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.

Guidance

Buttons and direct actions

Commit an explicit user action.

Use when

  • Use for explicit actions and commits; keep one action visually primary when several sit together.
  • Letting the user copy a code snippet, token or command to the clipboard in one click.
  • Pairing a compact icon-only trigger beside a value the user is likely to paste elsewhere.

Avoid when

  • Do not use button treatment for navigation-only view changes when links, tabs, or radios describe state more clearly.
  • The value is already editable and visible in a field with its own copy affordance; use input-copy instead.
  • The action does more than write to the clipboard; use a plain button instead.

Use instead

  • Link for navigation
  • Tabs or radios for view changes

Anti-patterns

  • Using a button for navigation-only state changes
  • Presenting multiple equally dominant primary actions
Anatomy
root copy-button-label
Theming hooks
copy-button copy-button-label

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
managed
Focus
managed
  • The accessible name stays constant across states; the state change is announced separately from a polite live region.
  • The icon inside the button is aria-hidden so screen readers read only the button's accessible name and the live-region announcement.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:ignore

Add wire:ignore to the component root because its behavior owns rendered DOM.

livewire-component.blade.php Blade
<div wire:ignore>
    {{-- Icon-only (the accessible name is `label`), and `show-label` for Copy → Copied text. --}}
    <div class="flex w-full max-w-md flex-col gap-4">
        <div class="flex items-center gap-3 rounded-md border border-border bg-muted ps-4 pe-2 py-2 text-sm text-foreground">
            <span class="flex-1 truncate font-mono">composer require jml/brok</span>
            <x-ui.copy-button value="composer require jml/brok" label="Copy command" />
        </div>
        <div class="flex items-center gap-3 rounded-md border border-border bg-muted ps-4 pe-2 py-2 text-sm text-foreground">
            <span class="flex-1 truncate font-mono">php artisan ui:add copy-button</span>
            <x-ui.copy-button value="php artisan ui:add copy-button" show-label variant="outline" />
        </div>
    </div>
</div>

Source

The exact, editable files ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/copy-button.blade.php Blade
@props([
    'value' => '',
    'timeout' => 2000,
    // Accessible name (icon form) or visible text (`show-label`) at rest.
    'label' => 'Copy',
    // Announced by the live region, and shown in place of `label` in the labelled form.
    'copiedLabel' => 'Copied',
    // Announced (and shown) when the clipboard write is refused: insecure context,
    // permission denied, or the document not focused.
    'failedLabel' => 'Copy failed',
    // Set true for "Copy" + icon that becomes "Copied" + check; false is the icon-only form.
    'showLabel' => false,
    // Passed to the composed button: any button variant/size/shape.
    'variant' => 'ghost',
    'size' => 'sm',
    'shape' => 'default',
])

@php
    // Accept a string, a backed enum or a Stringable for `variant`.
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant);
    // Explicit vocabulary, forwarded to the composed button; an unknown variant,
    // size or shape falls back to the default instead of leaking through.
    $variants = ['ghost', 'outline', 'secondary', 'default', 'link', 'destructive'];
    $sizes = ['sm', 'md', 'lg'];
    $shapes = ['default', 'pill'];
    $variant = in_array($variant, $variants, true) ? $variant : 'ghost';
    $size = in_array($size, $sizes, true) ? $size : 'sm';
    $shape = in_array($shape, $shapes, true) ? $shape : 'default';
@endphp

{{--
    Copy button — composes <x-ui.button>. The three states (copy / copied /
    failed) are grid-stacked icons that cross-fade, so the control never changes
    size. The accessible name stays constant; the state change is announced from
    the sibling live region, because the icon form's slot is aria-hidden inside
    the button. Consumer attributes land on the button itself.
--}}
<span
    data-slot="copy-button"
    x-data="uiCopyButton({ value: @js($value), timeout: @js((int) $timeout) })"
    x-bind:data-state="copied ? 'copied' : (failed ? 'failed' : 'idle')"
    class="contents"
>
    <span role="status" aria-live="polite" class="sr-only" x-text="copied ? @js(__($copiedLabel)) : (failed ? @js(__($failedLabel)) : '')"></span>

    <x-ui.button
        :variant="$variant"
        :size="$size"
        :shape="$shape"
        :icon-label="$showLabel ? null : $label"
        @click="copy()"
        ::class="failed ? 'text-destructive hover:text-destructive' : ''"
        {{ $attributes->merge(['class' => $showLabel ? '' : 'text-muted-foreground hover:text-foreground']) }}
    >
        @php
            // One stack of three grid-stacked marks, shared by both forms.
            $copyIcons = <<<'HTML'
            <span class="inline-grid size-4 shrink-0 place-items-center" aria-hidden="true">
                <svg
                    class="col-start-1 row-start-1 size-4 motion-safe:transition motion-safe:duration-fast motion-safe:ease-standard motion-reduce:transition-none"
                    :class="copied || failed ? 'opacity-0 motion-safe:scale-75' : 'opacity-100 motion-safe:scale-100'"
                    viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
                ><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
                <svg
                    class="col-start-1 row-start-1 size-4 text-success motion-safe:transition motion-safe:duration-fast motion-safe:ease-standard motion-reduce:transition-none"
                    :class="copied ? 'opacity-100 motion-safe:scale-100' : 'opacity-0 motion-safe:scale-75'"
                    viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
                ><polyline points="20 6 9 17 4 12"/></svg>
                <svg
                    class="col-start-1 row-start-1 size-4 motion-safe:transition motion-safe:duration-fast motion-safe:ease-standard motion-reduce:transition-none"
                    :class="failed ? 'opacity-100 motion-safe:scale-100' : 'opacity-0 motion-safe:scale-75'"
                    viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"
                ><path d="M18 6 6 18M6 6l12 12"/></svg>
            </span>
            HTML;
        @endphp

        @if ($showLabel)
            {{-- The marks ride in the button's icon slot so they centre beside the text. --}}
            <x-slot:icon>{!! $copyIcons !!}</x-slot:icon>
            <span data-slot="copy-button-label" x-text="copied ? @js(__($copiedLabel)) : (failed ? @js(__($failedLabel)) : @js(__($label)))">{{ __($label) }}</span>
        @else
            {!! $copyIcons !!}
        @endif
    </x-ui.button>
</span>
resources/js/ui/copy-button.js JS
/**
 * Copy button behavior.
 *
 * Copies a given text value (or the inner text of a referenced element) to the
 * clipboard, then toggles a "copied" state for a configurable timeout so the UI
 * can animate a copy → check icon transition and announce success to screen readers.
 * A refused write (insecure context, permission denied, document not focused)
 * sets "failed" for the same timeout instead of claiming the clipboard has the text.
 *
 * Self-registers on `alpine:init` so import order does not matter — import this
 * file once from your bundle (e.g. resources/js/ui/index.js).
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiCopyButton', (config = {}) => ({
        copied: false,
        failed: false,
        value: config.value ?? '',
        timeout: config.timeout ?? 2000,
        _t: null,

        async copy() {
            const text =
                this.value ||
                (this.$root.dataset.copyTarget
                    ? (document.querySelector(this.$root.dataset.copyTarget)?.innerText ?? '')
                    : '');

            try {
                if (navigator.clipboard?.writeText) {
                    await navigator.clipboard.writeText(text);
                } else {
                    // Fallback for older browsers / insecure contexts.
                    const ta = document.createElement('textarea');
                    ta.value = text;
                    ta.style.position = 'fixed';
                    ta.style.opacity = '0';
                    document.body.appendChild(ta);
                    ta.focus();
                    ta.select();
                    document.execCommand('copy');
                    document.body.removeChild(ta);
                }

                this.copied = true;
                this.failed = false;
            } catch (e) {
                this.copied = false;
                this.failed = true;
            }

            clearTimeout(this._t);
            this._t = setTimeout(() => {
                this.copied = false;
                this.failed = false;
            }, this.timeout);
        },

        destroy() {
            clearTimeout(this._t);
        },
    }));
});

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0