# Copy button

Copies a value to the clipboard and confirms it with a check and "Copied".

- **Import:** `import { CopyButton } from "@booleanpress/ui/copy-button"`
- **Radix Tooltip:** <https://www.radix-ui.com/primitives/docs/components/tooltip>
- **APG Button:** <https://www.w3.org/WAI/ARIA/apg/patterns/button/>
- **Page:** <https://ui.booleanpress.com/components/copy-button> · @booleanpress/ui 0.2.0

## Usage

Give `CopyButton` the text to copy as `value`, or a `getValue` function that returns it (or a promise of it) at the moment of the click.

```tsx
import { CopyButton } from "@booleanpress/ui/copy-button"

export function ApiKey({ apiKey }: { apiKey: string }) {
  return <CopyButton value={apiKey} label="Copy API key" />
}
```

It writes with the Clipboard API, and where that is missing or refused (an insecure page, a frame without permission) with a hidden text area and the browser's copy command. On success the icon turns to a check and it says "Copied" for `timeout` ms (2000), in its tooltip or label and in a polite live region; `onCopy` gets the text. If both ways fail it shows a cross and "Copy failed", and `onCopyError` is called. The fallback puts its hidden text area beside the button, so it works inside a `Dialog` or `Sheet`, whose focus trap would otherwise pull focus out of it. The same writer is exported as `copyText(text, container?)`, for copying from your own controls; pass an element inside the open dialog as `container`.

By default it is an icon button with a tooltip; `label` names what it copies. `showLabel` shows "Copy", then "Copied", beside the icon. `size` is `xs` (24 px, for inside an `InputGroup`), `sm`, `default` or `lg`; `variant` is Button's (`ghost` for the icon, `outline` with the label).

## Examples

### Icon only

An icon button beside a message ID; the tooltip says "Copy", then "Copied".

```tsx
import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonIconOnly() {
  return (
    <div className="flex items-center gap-2 text-sm">
      <span className="font-mono">msg_01J9X4T2QZ</span>
      <CopyButton value="msg_01J9X4T2QZ" label="Copy message ID" size="sm" />
    </div>
  )
}
```

### With label

`showLabel`: the text changes to "Copied" with the check.

```tsx
import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonWithLabel() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <CopyButton value="smtp.example.com" showLabel />
      <CopyButton value="smtp.example.com" showLabel variant="secondary" size="sm" />
    </div>
  )
}
```

### In an input group

An `xs` copy button at the end of a read-only API key field.

```tsx
import { CopyButton } from "@booleanpress/ui/copy-button"
import { InputGroup, InputGroupAddon, InputGroupInput } from "@booleanpress/ui/input-group"
import { Label } from "@booleanpress/ui/label"

const KEY = "bp_live_4f2a9c7e1d8b6053"

export default function CopyButtonInInputGroup() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="api-key">API key</Label>
      <InputGroup>
        <InputGroupInput id="api-key" value={KEY} readOnly className="font-mono" />
        <InputGroupAddon align="inline-end">
          <CopyButton value={KEY} label="Copy API key" size="xs" />
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}
```

### Custom timeout

`timeout={5000}` and `getValue`; `onCopy` counts the copies.

```tsx
import * as React from "react"
import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonCustomTimeout() {
  const [copies, setCopies] = React.useState(0)

  return (
    <div className="flex flex-col items-start gap-2">
      <CopyButton
        showLabel
        timeout={5000}
        getValue={() => "v=spf1 include:_spf.example.com ~all"}
        onCopy={() => setCopies((count) => count + 1)}
      />
      <p className="text-sm text-muted-foreground">
        The SPF record stays “Copied” for five seconds. Copied {copies} {copies === 1 ? "time" : "times"}.
      </p>
    </div>
  )
}
```

### Disabled

`disabled`: the button cannot be pressed and copies nothing.

```tsx
import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonDisabled() {
  return (
    <div className="flex items-center gap-2">
      <CopyButton disabled value="bp_live_4f2a9c7e1d8b6053" label="Copy API key" />
      <CopyButton disabled showLabel value="bp_live_4f2a9c7e1d8b6053" />
    </div>
  )
}
```

### Copy fails

A `getValue` that rejects: the button shows a cross and "Copy failed", and `onCopyError` receives the error.

```tsx
import * as React from "react"
import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonCopyFails() {
  const [error, setError] = React.useState("")

  return (
    <div className="flex flex-col items-start gap-2">
      <CopyButton
        showLabel
        // The value cannot be read, so nothing reaches the clipboard: the button shows a cross and says "Copy failed".
        getValue={() => Promise.reject(new Error("The backup code could not be read."))}
        onCopyError={(reason) => setError(reason instanceof Error ? reason.message : "Copy failed")}
      />
      <p className="min-h-5 text-sm text-destructive-strong">{error}</p>
    </div>
  )
}
```

## Accessibility

**Semantics.** A native `button`, followed by a visually hidden `<output>` (a polite status region) that says "Copied" or "Copy failed" after a press.

**Labels.** The icon button is named by `label`, or the provider's `copy` string; its name does not change when it has copied. With `showLabel`, the visible "Copy" or "Copied" is the name. "Copied" and "Copy failed" are the provider's `copied` and `copyFailed` strings.

**Focus.** Focus stays on the button through the copy, the fallback included, inside a dialog too. The tooltip opens on hover and keyboard focus, and stays open with "Copied" while the check shows, until Escape closes it.

**Known limits.**

- The Clipboard API needs a secure page (HTTPS or localhost); the fallback depends on the browser still supporting the copy command.
- Screen readers announce "Copied" once per copy; two presses within the timeout are announced once.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Enter | Copies the value. |
| Space | Copies the value. |
| Escape | Closes the tooltip, "Copied" included. |

## API

### CopyButton

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `getValue` | `(() => string \| Promise<string>)` |  | Returns the text to copy at the moment of the click, for a value that is not known while rendering. Wins over `value`. |
| `label` | `string` |  | The icon-only button's accessible name, naming what it copies ("Copy API key"). The provider's `copy` string by default. |
| `loading` | `boolean` |  | Shows the spinner in place of the leading icon, sets `aria-busy` and `aria-disabled` and ignores presses (a click, Enter, Space, a form's submission), while the button keeps its focus and its place in the tab order. With `asChild` the child (a link) gets the same. |
| `onCopy` | `((text: string) => void)` |  | Called with the copied text once it is on the clipboard. |
| `onCopyError` | `((error: unknown) => void)` |  | Called when neither the Clipboard API nor the fallback could copy. |
| `raised` | `boolean \| null` |  |  |
| `rounded` | `boolean \| null` |  | A circle (a pill with the label). |
| `severity` | `"success" \| "info" \| "warning" \| "help" \| "danger" \| "contrast" \| null` |  | The colour of the `default`, `outline`, `ghost` and `link` variants, as Button's. |
| `showLabel` | `boolean` | `false` | Shows "Copy" (then "Copied") beside the icon instead of the icon alone. |
| `size` | `"default" \| "xs" \| "sm" \| "lg"` |  | `xs` 24 px, `sm` 28 px, `default`, `lg` 42 px. The provider's `controlSize` when left out. |
| `timeout` | `number` | `2000` | Milliseconds the check and "Copied" stay. 2000 by default. |
| `tooltip` | `boolean` | `true` | The icon-only button's tooltip: "Copy", then "Copied". `true` by default. |
| `tooltipSide` | `"top" \| "bottom" \| "left" \| "right"` | `top` | The tooltip's side: `top` (default), `right`, `bottom` or `left`. |
| `value` | `string` |  | The text to copy. |
| `variant` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` |  | The emphasis, as Button's: `ghost` for the icon button, `outline` with the label, by default. |

**Also exported:** `copyText`, a helper the parts use.

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="copy-button"` (CopyButton), and `data-state`.

**Provider strings:** `copied`, `copyFailed`, `copy` (`BooleanUIProvider`'s `strings`).

## Theming
