# CopyButton

> Icon button that copies text to the clipboard and shows a check for 1.5 seconds, with a screen-reader announcement and a fallback; CopyField pairs it with a read-only, always left-to-right input for keys and URLs.

Source: https://docs.nasaqui.com/components/copy-button

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/copy-button.json
```

A small button that puts a string on the clipboard and confirms it. The icon turns into a check for about
1.5 seconds and a polite live region announces the result, so the confirmation is not visual only.
`CopyField` wraps it with a read-only input for values people copy: API keys, invite links, URLs.

## When to use

- A value the user will paste elsewhere: a key, a link, a command.
- `CopyField` when the value should be visible and selectable next to its label.

## When not to use

- Sharing to other apps: use the Web Share API from a normal [`Button`](https://docs.nasaqui.com/components/button).
- Long confirmations or undo: fire a [toast](https://docs.nasaqui.com/components/toast) from `onCopy`.

## Import

```tsx
import { CopyButton, CopyField } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
import { CopyButton, CopyField, Field, FieldLabel } from "@fadymondy/nasaq/web";

export function Example() {
  return (
    <>
      <CopyButton value="npm i @fadymondy/nasaq" />
      <Field>
        <FieldLabel>API key</FieldLabel>
        <CopyField value="nq_live_4f9c2b7a1d8e" label="API key" />
      </Field>
    </>
  );
}
```

## Anatomy

```
CopyButton                      data-slot="copy-button"  (data-copied)
└─ span role="status"           data-slot="copy-button-status"  (sr-only)

CopyField                       data-slot="copy-field"  (an InputGroup)
├─ InputGroupInput              readOnly, dir="ltr"
└─ InputGroupAddon (end)
   └─ CopyButton
```

## API

### CopyButton

Also accepts every `Button` prop except `onClick`, `onCopy`, `value` and `children`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string \| (() => string)` | required | Text to copy, or a function called at click time. |
| `label` | `string` | "Copy" / "نسخ" | Accessible name when icon-only. |
| `copiedLabel` | `string` | "Copied to clipboard" / "تم النسخ إلى الحافظة" | Announced after a copy. |
| `failedLabel` | `string` | "Could not copy" / "تعذر النسخ" | Announced when copying fails. |
| `resetAfter` | `number` | `1500` | Milliseconds the check stays. |
| `onCopy` | `(text: string) => void` | none | After a successful copy. |
| `onCopyError` | `() => void` | none | When both the Clipboard API and the fallback fail. |
| `children` | `ReactNode` | none | Visible text. Omit for an icon-only button. |
| `variant` | `ButtonProps["variant"]` | `"ghost"` | |
| `size` | `ButtonProps["size"]` | `"icon-sm"` icon-only, else `"sm"` | |

### CopyField

Also accepts `InputGroup` props (except `onCopy`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | required | Shown and copied. |
| `label` | `string` | none | `aria-label` of the input. Skip it when a `FieldLabel` names the input. |
| `copyLabel` | `string` | "Copy" / "نسخ" | The button's accessible name. |
| `copiedLabel` | `string` | localised | Announcement text. |
| `onCopy` | `(text: string) => void` | none | |
| `inputProps` | input props | none | Extra props for the input, for example `id`. |

### copyText

`copyText(text: string): Promise<boolean>`: the Clipboard API, then a hidden-textarea `execCommand("copy")` fallback. Resolves `false` when both fail.

## Examples

Visible label and a toast:

```tsx
import { CopyButton } from "@fadymondy/nasaq/web";

export function InviteLink({ url }: { url: string }) {
  return (
    <CopyButton value={url} variant="secondary" onCopy={() => console.log("copied")}>
      Copy link
    </CopyButton>
  );
}
```

Arabic:

```tsx
import { CopyField } from "@fadymondy/nasaq/web";

export function ArabicKey() {
  return <CopyField value="nq_live_4f9c2b7a1d8e" label="مفتاح API" copiedLabel="تم نسخ المفتاح" />;
}
```

## Accessibility

| Key | Action |
| --- | --- |
| Enter, Space | Copies. |
| Focus in CopyField | Selects the whole value. |

- Icon-only buttons are named by `label`.
- The result is spoken through `role="status"` (polite). The button label does not change.
- Localise `label`, `copiedLabel` and `failedLabel` when you pass your own; defaults follow the Nasaq locale.

## RTL & i18n

- `CopyField` forces `dir="ltr"` and `text-start` on the value: keys and URLs must not reorder in Arabic.
- The button sits at the inline end, so it appears on the left in RTL.

## Styling & tokens

Uses Button tokens and `text-nq-success-text` while `[data-copied]`. Extend with `className`.

## Do / Don't

- Do put the copy button next to the value it copies.
- Do not copy secrets you have not shown; the user should see what they copied.
- Do not rely on the icon change alone to confirm; keep the live region.

## Related

- [Button](https://docs.nasaqui.com/components/button)
- [InputGroup](https://docs.nasaqui.com/components/input-group)
- [Field](https://docs.nasaqui.com/components/field)

## Lab

https://docs.nasaqui.com/?path=/docs/components-actions-copy-button--docs

## Code

### React

```tsx
import { CopyButton, CopyField, Field, FieldLabel } from "@fadymondy/nasaq/web";

export function Example() {
  return (
    <>
      <CopyButton value="npm i @fadymondy/nasaq" />
      <Field>
        <FieldLabel>API key</FieldLabel>
        <CopyField value="nq_live_4f9c2b7a1d8e" label="API key" />
      </Field>
    </>
  );
}
```

### shadcn

```tsx
import { CopyButton, CopyField } from "@/components/ui/copy-button";
import { Field, FieldLabel } from "@/components/ui/field";

export function Example() {
  return (
    <>
      <CopyButton value="npm i @fadymondy/nasaq" />
      <Field>
        <FieldLabel>API key</FieldLabel>
        <CopyField value="nq_live_4f9c2b7a1d8e" label="API key" />
      </Field>
    </>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqCopyButton, NqCopyField } from "@fadymondy/nasaq/vue";
</script>

<template>
  <div class="flex flex-col gap-4">
    <NqCopyButton value="sk_live_51Nasaq" label="Copy API key" />
    <NqCopyButton value="https://nasaq.app/invite/9">Copy link</NqCopyButton>
    <NqCopyField value="https://nasaq.app/invite/9" label="Invite link" />
  </div>
</template>
```

### Blade

```blade
<div class="flex flex-col gap-4">
    <x-nq::copy-button value="sk_live_51Nasaq" label="Copy API key" />
    <x-nq::copy-button value="https://nasaq.app/invite/9">Copy link</x-nq::copy-button>
    <x-nq::copy-button.field value="https://nasaq.app/invite/9" label="Invite link" />
</div>
```

### HTML + Alpine

```html
<div class="flex flex-col gap-4">
    <span class="contents" x-data="nqCopyButton('sk_live_51Nasaq', 1500)">
    <button data-slot="copy-button"
     type="button"                         aria-label="Copy API key" x-on:click="copy()" x-bind:data-copied="state === &#039;copied&#039; ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 data-copied:text-nq-success-text">
        <svg aria-hidden="true" x-show="state !== 'copied'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <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>        <svg aria-hidden="true" x-show="state === 'copied'" style="display: none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></button>
    <span data-slot="copy-button-status" role="status" aria-live="polite" class="sr-only"
        data-copied-label="Copied to clipboard" data-failed-label="Could not copy"
        x-text="state === 'copied' ? $el.dataset.copiedLabel : state === 'failed' ? $el.dataset.failedLabel : ''"></span>
</span>
    <span class="contents" x-data="nqCopyButton('https:\/\/nasaq.app\/invite\/9', 1500)">
    <button data-slot="copy-button"
     type="button"                         x-on:click="copy()" x-bind:data-copied="state === &#039;copied&#039; ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control-sm px-2.5 data-copied:text-nq-success-text">
        <svg aria-hidden="true" x-show="state !== 'copied'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <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>        <svg aria-hidden="true" x-show="state === 'copied'" style="display: none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>        Copy link</button>
    <span data-slot="copy-button-status" role="status" aria-live="polite" class="sr-only"
        data-copied-label="Copied to clipboard" data-failed-label="Could not copy"
        x-text="state === 'copied' ? $el.dataset.copiedLabel : state === 'failed' ? $el.dataset.failedLabel : ''"></span>
</span>
    <div data-slot="copy-field" class="contents">
    <div role="group" data-slot="input-group"
    class="group/input-group flex h-control min-h-[var(--nq-touch-min,0px)] w-full min-w-0 items-center overflow-hidden rounded-control border border-input bg-card text-body text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50"><input data-slot="input-group-input"  dir="ltr"     readonly="readonly" value="https://nasaq.app/invite/9" aria-label="Invite link" x-on:focus="$el.select()" class="h-full min-w-0 flex-1 border-0 bg-transparent px-3 text-body text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed pointer-coarse:text-[16px] text-start" />
        <div data-slot="input-group-addon" data-align="end"
    class="flex h-full shrink-0 items-center gap-1.5 text-body-sm text-muted-foreground [&_svg]:size-4 order-last ps-1 pe-3"><span class="contents" x-data="nqCopyButton('https:\/\/nasaq.app\/invite\/9', 1500)">
    <button data-slot="copy-button"
     type="button"                         aria-label="Copy" x-on:click="copy()" x-bind:data-copied="state === &#039;copied&#039; ? &#039;&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control-sm p-0 data-copied:text-nq-success-text">
        <svg aria-hidden="true" x-show="state !== 'copied'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <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>        <svg aria-hidden="true" x-show="state === 'copied'" style="display: none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></button>
    <span data-slot="copy-button-status" role="status" aria-live="polite" class="sr-only"
        data-copied-label="Copied to clipboard" data-failed-label="Could not copy"
        x-text="state === 'copied' ? $el.dataset.copiedLabel : state === 'failed' ? $el.dataset.failedLabel : ''"></span>
</span></div></div>
</div>
</div>
```
