Skip to content
Brok UI

Loading…

No results

Input Copy

Open source

A read-only input paired with a copy-to-clipboard trigger that cross-fades to an animated check. Icon or labelled-button trigger, alignable to either inline edge.

Version
v1.1.4
Stability
stable
License
MIT
Related
Input
Code Line
Masked Input

Preview

Variant
Align
Disabled
previews.components.input-copy.default.blade.php Blade
<div class="w-80 max-w-full space-y-4">
    <x-ui.input-copy
        label="{{ __('API key') }}"
        value="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
    />

    <x-ui.input-copy
        variant="button"
        value="https://brok.test/r/registry.json"
        :copyLabel="__('Copy')"
        :copiedLabel="__('Copied')"
    />
</div>

Variant options

Icon Current
Button Current

Align options

Right Current
Start Current
Center Current
End Current

Installation

terminal
php artisan ui:add input-copy

Note

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

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

Registry contract

php artisan ui:add input-copy 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/input-copy.blade.php
Registry dependencies
copy-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.

input-copy.md
# Brok UI: Input Copy (`input-copy`)

A read-only input paired with a copy-to-clipboard trigger that cross-fades to an animated check. Icon or labelled-button trigger, alignable to either inline edge.

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

## Install

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

## Usage

```blade
<div class="w-80 max-w-full space-y-4">
    <x-ui.input-copy
        label="{{ __('API key') }}"
        value="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
    />

    <x-ui.input-copy
        variant="button"
        value="https://brok.test/r/registry.json"
        :copyLabel="__('Copy')"
        :copiedLabel="__('Copied')"
    />
</div>
```

## Props

- `value` (string, default ``) — The read-only text shown in the field and written to the clipboard.
- `label` (mixed|null, default `null`) — Optional visible label rendered above the field.
- `variant` (icon|button, default `icon`) — Trigger form: icon-only, or button with a visible copy/copied label.
- `align` (string, default `right`) — Which inline edge the trigger sits on; left reverses the row so it leads instead.
- `disabled` (bool, default `false`) — Disables both the field and the copy trigger.
- `copyLabel` (string, default `Copy`) — Accessible name (and, in button variant, visible text) at rest.
- `copiedLabel` (string, default `Copied`) — Announced and shown after a successful copy.
- `timeout` (int, default `2000`) — Milliseconds the copied state stays shown before reverting to idle.
- `id` (mixed|null, default `null`) — Field id; generated when omitted so the optional label's for attribute always resolves.

## Use when

- Use when users must enter freeform information that cannot be reliably selected from a list.
- Showing a read-only value such as an API key or invite link with an obvious copy affordance beside it.
- Needing a labelled field-and-button pair instead of a bare copy-button icon.

## Avoid when

- Do not choose a freeform field when a constrained choice would reduce errors or cognitive load.
- The value must also be editable; use input with copy-button placed beside it instead.
- No field chrome is wanted, only a trigger; use copy-button on its own.

## Anti-patterns

- Using freeform entry for a bounded answer set

## Rules

- Use the `<brok:input-copy>` tag (or `<x-ui.input-copy>`) 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/input-copy
- Registry JSON (files, props, contract): https://brokui.dev/r/open/input-copy.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 read-only text shown in the field and written to the clipboard.
label mixed | null null Optional visible label rendered above the field.
variant icon | button icon Trigger form: icon-only, or button with a visible copy/copied label.
align string right Which inline edge the trigger sits on; left reverses the row so it leads instead.
disabled bool false Disables both the field and the copy trigger.
copyLabel string Copy Accessible name (and, in button variant, visible text) at rest.
copiedLabel string Copied Announced and shown after a successful copy.
timeout int 2000 Milliseconds the copied state stays shown before reverting to idle.
id mixed | null null Field id; generated when omitted so the optional label's for attribute always resolves.

Slots

  • default — Unused; the field and trigger render from props only.

Data slots

Stable hooks for CSS overrides and browser tests.

input-copy input-copy-field input-copy-trigger

Behavior

  • The field is read-only; the only way to act on it is the copy trigger.
  • The trigger cross-fades from a clipboard icon to a check mark on success and reverts to idle after timeout milliseconds.
  • Shares the same uiCopyButton Alpine behaviour as copy-button, installed with the copy-button dependency.
  • 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

Freeform text input

Collect unpredictable freeform information.

Use when

  • Use when users must enter freeform information that cannot be reliably selected from a list.
  • Showing a read-only value such as an API key or invite link with an obvious copy affordance beside it.
  • Needing a labelled field-and-button pair instead of a bare copy-button icon.

Avoid when

  • Do not choose a freeform field when a constrained choice would reduce errors or cognitive load.
  • The value must also be editable; use input with copy-button placed beside it instead.
  • No field chrome is wanted, only a trigger; use copy-button on its own.

Use instead

  • Radio or checkbox for bounded choices
  • Select or combobox for known options

Anti-patterns

  • Using freeform entry for a bounded answer set
Anatomy
root input-copy-field input-copy-trigger
Theming hooks
input-copy input-copy-field input-copy-trigger

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • The trigger's accessible name announces Copy or Copied through aria-label, without relying on colour alone.
  • When no label prop is set the read-only field gets an aria-label so it is not announced as an empty control.
  • 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:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="input-copy-{{ $record->id }}">
    <div class="w-80 max-w-full space-y-4">
        <x-ui.input-copy
            label="{{ __('API key') }}"
            value="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
        />
    
        <x-ui.input-copy
            variant="button"
            value="https://brok.test/r/registry.json"
            :copyLabel="__('Copy')"
            :copiedLabel="__('Copied')"
        />
    </div>
</div>

Validation

Validation support: native. Keep the error message connected with aria-describedby.

livewire-form.blade.php Blade
<form wire:submit="save" class="space-y-2">
    <brok:input-copy
        wire:model="value"
        :aria-invalid="$errors->has('value') ? 'true' : 'false'"
        aria-describedby="value-error"
    />

    @error('value')
        <p id="value-error" role="alert">{{ $message }}</p>
    @enderror

    <brok:button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove>Save</span>
        <span wire:loading>Saving…</span>
    </brok:button>
</form>

Source

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

resources/views/components/ui/input-copy.blade.php Blade
@props([
    'value' => '',
    'label' => null,
    'variant' => 'icon',
    'align' => 'right',
    'disabled' => false,
    'copyLabel' => 'Copy',
    'copiedLabel' => 'Copied',
    'timeout' => 2000,
    'id' => null,
])

@php
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $variant = $styles['normalizeVariant']($variant);
    // Normalise the two-way axes. `variant` decides whether the trigger shows a
    // visible label; `align` decides which inline edge it sits on (logical, so it
    // mirrors under dir="rtl").
    $variant = in_array($variant, ['icon', 'button'], true) ? $variant : 'icon';
    $align = $align === 'left' ? 'left' : 'right';
    $fieldId = $id ?? 'input-copy-'.\Illuminate\Support\Str::random(6);
@endphp

{{--
    Input Copy — a read-only field paired with a copy-to-clipboard trigger that
    cross-fades to an animated check on success. The behaviour is the shared
    `uiCopyButton` Alpine data (installed with the `copy-button` dependency), so
    this component only owns layout + the icon/label swap. The trigger lives at the
    inline-end edge by default; `align="left"` reverses the row so it leads instead.
--}}
<div
    data-slot="input-copy"
    data-variant="{{ $variant }}"
    data-align="{{ $align }}"
    x-data="uiCopyButton({ value: @js((string) $value), timeout: @js((int) $timeout) })"
    {{ $attributes->only('class')->merge(['class' => 'w-full']) }}
>
    @if ($label)
        <label for="{{ $fieldId }}" class="mb-2 block text-sm font-medium text-foreground">{{ __($label) }}</label>
    @endif

    <div @class([
        'flex items-center gap-1 rounded-md border border-input bg-background ps-3 pe-1 shadow-sm transition-[color,box-shadow] focus-within:border-ring focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background motion-reduce:transition-none',
        'flex-row-reverse ps-1 pe-3' => $align === 'left',
        'opacity-50' => $disabled,
    ])>
        <input
            id="{{ $fieldId }}"
            type="text"
            value="{{ $value }}"
            readonly
            @disabled($disabled)
            data-slot="input-copy-field"
            @unless ($label) aria-label="{{ __($copyLabel ?? 'Copyable value') }}" @endunless
            class="h-9 w-full min-w-0 flex-1 bg-transparent text-sm text-foreground outline-none selection:bg-primary/20 disabled:cursor-not-allowed"
            {{ $attributes->except('class') }}
        />

        <button
            type="button"
            @click="copy()"
            @disabled($disabled)
            x-bind:aria-label="copied ? @js(__($copiedLabel)) : @js(__($copyLabel))"
            data-slot="input-copy-trigger"
            @class([
                'group/copy m-1 inline-flex shrink-0 items-center justify-center gap-2 rounded-sm text-sm font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
                'size-7' => $variant === 'icon',
                'h-7 px-2' => $variant === 'button',
            ])
        >
            {{-- Screen-reader live region: announces the copied state to assistive tech. --}}
            <span aria-live="polite" class="sr-only" x-text="copied ? @js(__($copiedLabel)) : ''"></span>

            {{-- Copy (clipboard) icon — shown while idle. --}}
            <svg
                x-show="!copied"
                x-transition:enter="transition ease-out duration-150 motion-reduce:transition-none"
                x-transition:enter-start="opacity-0 scale-75"
                x-transition:enter-end="opacity-100 scale-100"
                xmlns="http://www.w3.org/2000/svg"
                class="size-4 shrink-0"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
                aria-hidden="true"
            >
                <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
                <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
            </svg>

            {{-- Check icon — shown after a successful copy. --}}
            <svg
                x-show="copied"
                x-transition:enter="transition ease-out duration-150 motion-reduce:transition-none"
                x-transition:enter-start="opacity-0 scale-50"
                x-transition:enter-end="opacity-100 scale-100"
                xmlns="http://www.w3.org/2000/svg"
                class="size-4 shrink-0 text-success"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
                aria-hidden="true"
            >
                <path d="M20 6 9 17l-5-5" />
            </svg>

            @if ($variant === 'button')
                <span x-text="copied ? @js(__($copiedLabel)) : @js(__($copyLabel))">{{ __($copyLabel) }}</span>
            @endif
        </button>
    </div>
</div>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.4
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