# Copy

A button, a field and a secret field that copy a value to the clipboard.

```tsx
import { CopyField, Field, SecretField } from 'ferry-ui'

export default function CopyHero() {
  return (
    <div className="mx-auto flex w-full max-w-md flex-col gap-5">
      <Field label="Project ID">
        <CopyField value="prj_7Hq2kLx9Vd3mN4" what="project ID" />
      </Field>
      <Field label="Secret key" hint="Keep this key on your server.">
        <SecretField value="sk_demo_4f9a2c7e1b8d3f6a" what="secret key" />
      </Field>
    </div>
  )
}
```

## Usage guidelines

- **For a value that the user pastes in another place.** For example an identifier, a URL or a key.
- **Each credential is a secret.** Show an API key, a password or a recovery code in `SecretField`.
- **A command is a code block.** For a command or a code sample, use [Code Block](/docs/components/code-block).
- **Mount the providers.** A copy that fails shows a [Toast](/docs/components/toast), and the icon buttons have a [Tooltip](/docs/components/tooltip).
- **Build your own control with the hook.** [useCopy](/docs/utilities/use-copy) gives the same behavior to your component.

## Anatomy

Import the component that you need. Each one has one part.

```tsx title="Anatomy"

<CopyButton value="" what="" />
<CopyField value="" what="" />
<SecretField value="" what="" />
```

| Component | Role |
| --- | --- |
| `CopyButton` | A small button next to a value. |
| `CopyField` | A read-only field with a Copy button. |
| `SecretField` | A read-only field that masks its value. It has a reveal button and a Copy button. |

## Examples

### Icon button

By default, `CopyButton` shows only an icon and a tooltip. Set `what` to name the value. After a copy, the icon is a check mark for 1.5 seconds. Use `variant="ghost"` in a dense row or a table.

```tsx
import { CopyButton } from 'ferry-ui'

const INVOICES = ['INV-2026-0142', 'INV-2026-0143']

export default function CopyButtonIcon() {
  return (
    <ul className="w-full max-w-xs divide-y rounded-lg border bg-surface-100">
      {INVOICES.map((invoice) => (
        <li key={invoice} className="flex items-center justify-between gap-3 px-4 py-2">
          <span className="font-mono text-[13px] text-foreground">{invoice}</span>
          {/* `what` gives the tooltip and the accessible name: "Copy invoice number". */}
          <CopyButton value={invoice} what="invoice number" variant="ghost" />
        </li>
      ))}
    </ul>
  )
}
```

### Button with a label

`label` gives the button a visible text. After a copy, the text is "Copied" for 1.5 seconds. If the label does not name the value, add `what` for screen readers.

```tsx
import { CopyButton } from 'ferry-ui'

const INVITE_LINK = 'https://app.example.com/invite/8f2c41d9'

export default function CopyButtonLabel() {
  return (
    <>
      <CopyButton value={INVITE_LINK} label="Copy invite link" />
      <CopyButton value={INVITE_LINK} label="Copy" what="invite link" variant="ghost" />
      <CopyButton value={INVITE_LINK} label="Copy" what="invite link" size="sm" />
    </>
  )
}
```

### Copy field

`CopyField` selects its text when it gets the focus. The `size` prop is `md` (34px) by default, or `sm` (30px). Set `mono` to `false` for plain words. The field cuts a long value, but the button copies the full value.

```tsx
import { CopyField } from 'ferry-ui'

export default function CopyFieldSizes() {
  return (
    <div className="mx-auto flex w-full max-w-md flex-col gap-3">
      <CopyField value="https://api.example.com/v2" what="API URL" aria-label="API URL, medium" />
      <CopyField value="https://api.example.com/v2" what="API URL" size="sm" aria-label="API URL, small" />
      <CopyField value="billing@example.com" what="billing email" mono={false} aria-label="Billing email" />
      <CopyField
        value="https://hooks.example.com/incoming/workspace-acme/channel-announcements/a8f3e21c9b7d4e6f"
        what="webhook URL"
        aria-label="Webhook URL"
      />
    </div>
  )
}
```

### Secret field

`SecretField` hides the value until the user reveals it. Copy takes the real value and does not reveal it. `mask` replaces the default dots.

```tsx
import { Field, SecretField } from 'ferry-ui'

export default function CopySecret() {
  return (
    <div className="mx-auto flex w-full max-w-md flex-col gap-5">
      <Field label="Secret key">
        <SecretField value="sk_demo_4f9a2c7e1b8d3f6a" what="secret key" />
      </Field>
      <Field label="Recovery code" hint="The mask shows the last four characters.">
        <SecretField value="4F7K-9QXM-2B8R-TL6P" what="recovery code" mask="••••-••••-••••-TL6P" />
      </Field>
    </div>
  )
}
```

### Controlled reveal

To control the reveal state, pass `revealed` and `onRevealedChange`. Here one [Switch](/docs/components/switch) shows or hides the two keys.

```tsx
import * as React from 'react'
import { Label, SecretField, Switch } from 'ferry-ui'

export default function CopySecretControlled() {
  const [shown, setShown] = React.useState(false)

  return (
    <div className="mx-auto flex w-full max-w-md flex-col gap-3">
      <div className="flex items-center gap-2">
        <Switch id="show-keys" checked={shown} onCheckedChange={setShown} />
        <Label htmlFor="show-keys">Show keys</Label>
      </div>
      <SecretField
        value="sk_demo_primary_7c1e9b4a"
        what="primary key"
        aria-label="Primary key"
        revealed={shown}
        onRevealedChange={setShown}
      />
      <SecretField
        value="sk_demo_secondary_2d8f3a6c"
        what="secondary key"
        aria-label="Secondary key"
        revealed={shown}
        onRevealedChange={setShown}
      />
    </div>
  )
}
```

### Custom texts

`labels` replaces the built-in texts of the three components. Use it to translate them.

```tsx
import { CopyButton, SecretField, type CopyLabels } from 'ferry-ui'

// Module level: all the copy components of the app use this object. An entry that you leave out keeps its default.
const LABELS: Partial<CopyLabels> = {
  copy: 'Duplicate',
  copied: 'Done',
  copyWhat: (what) => `Duplicate ${what}`,
  reveal: 'Show',
  hide: 'Mask',
  revealWhat: (what) => `Show ${what ?? 'value'}`,
  hideWhat: (what) => `Mask ${what ?? 'value'}`,
}

export default function CopyLabelsDemo() {
  return (
    <div className="mx-auto flex w-full max-w-md flex-col gap-3">
      <div className="flex items-center gap-2">
        <span className="font-mono text-[13px] text-foreground">INV-2026-0142</span>
        <CopyButton value="INV-2026-0142" what="invoice number" variant="ghost" labels={LABELS} />
      </div>
      <SecretField value="sk_demo_4f9a2c7e1b8d3f6a" what="secret key" aria-label="Secret key" labels={LABELS} />
    </div>
  )
}
```

## Accessibility

- Always set `what` on an icon button. It gives the accessible name, for example "Copy API key".
- After a copy, screen readers read "Copied".
- Give each field a label. Use [Field](/docs/components/field), `FormRow` of [Form Card](/docs/components/form-card), or `aria-label`.

## API reference

### CopyButton

`CopyButton` also accepts the props of [Button](/docs/components/button), but not `onClick` and `children`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  | The exact text written to the clipboard. |
| `label` | `string` |  | Visible label (e.g. "Copy"). When set, the button shows the label and flips to "Copied" (`labels.copied`) for 1.5s. When omitted, the button is icon-only, gets `aria-label="Copy <what>"` (`labels.copyWhat`) and a tooltip. |
| `what` | `string` |  | What is being copied ("URL", "invoice number", "API key"). Icon-only: the accessible name and tooltip become "Copy API key" — always set it there. Labelled: it is appended to the accessible name ("Copy" → "Copy API key") so several Copy buttons on one screen stay distinguishable for screen readers; omit it when the label already says what is copied ("Copy link"). |
| `onCopy` | `((value: string) => void)` |  | Called with `value` after each click, once the clipboard write has been attempted. It also fires when the write failed (the user then sees an error toast), so use it for analytics, not to confirm success. Replaces the native clipboard-event `onCopy`. |
| `labels` | `Partial<CopyLabels>` |  | Overrides of the built-in texts (`Copy <what>`, `Copied`), to translate or reword them. See `CopyLabels`; unset entries keep their English default. |
| `variant` | `"link" \| "default" \| "primary" \| "outline" \| "ghost" \| "destructive" \| "destructive-solid" \| "danger" \| "danger-solid" \| "warning" \| "dashed"` |  | Look. `default` (outlined neutral, the default), `primary` (solid: the one main action of a view), `outline` (transparent with a border, on tinted surfaces), `ghost` (borderless, dense rows and toolbars), `destructive` (red outline: starts a destructive action), `destructive-solid` (solid red: the confirm button of a destructive dialog only), `warning` (amber outline), `link` (text link look), `dashed` (filter buttons). `danger` and `danger-solid` are deprecated aliases of `destructive` and `destructive-solid`. |
| `size` | `"tiny" \| "sm" \| "md" \| "lg" \| "icon-tiny" \| "icon" \| "icon-md" \| "icon-lg"` |  | Height. `tiny` 26px (dense inline actions), `sm` 30px (default: toolbars, table rows), `md` 34px (next to form fields), `lg` 38px. Square, icon-only sizes of the same heights: `icon-tiny` 26px, `icon` 30px, `icon-md` 34px, `icon-lg` 38px (they need an `aria-label`). |
| `shape` | `"default" \| "pill"` |  | Corners: `default` (6px radius) or `pill` (fully rounded: top-bar actions, search triggers). |
| `asChild` | `boolean` |  | Style the single child element (usually an `<a>` or a router link) as the button instead of rendering a `<button>`. `icon` / `iconRight` are rendered inside the child. A link has no native disabled state, so `disabled` and `loading` map to ARIA on the child: `aria-disabled` (dimmed, pointer events off), `tabIndex={-1}` and, for `loading`, `aria-busy` plus the spinner. `type` is ignored. |
| `loading` | `boolean` |  | Shows a spinner in place of `icon`, disables the button and sets `aria-busy`. Use it while the action triggered by this button is in flight. |
| `icon` | `ReactNode` |  | Leading icon (rendered before children). Pass a bare lucide icon: it is sized for you. |
| `iconRight` | `ReactNode` |  | Trailing icon (rendered after children). Pass a bare lucide icon: it is sized for you. |

### CopyField

`CopyField` accepts only the props of this table.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  | The value shown in the field and written to the clipboard. |
| `id` | `string` |  | Id of the input, so a `<label htmlFor>` (or a FormRow `htmlFor`) can point at it. |
| `mono` | `boolean` | `true` | Monospace value (default `true`): identifiers, URLs, keys. Set `false` for plain words. |
| `what` | `string` |  | What is being copied, used in the copy button's accessible name ("Copy project ID"). |
| `size` | `"sm" \| "md"` | `md` | Field height: `sm` 30px (dense cards, toolbars) or `md` 34px (default, form rows). |
| `className` | `string` |  | Merged onto the wrapper element. |
| `aria-label` | `string` |  | Accessible name of the input when no `<label htmlFor>` points at it. |
| `aria-describedby` | `string` |  | Id(s) of the helper text describing the field. |
| `onCopy` | `((value: string) => void)` |  | Called with `value` each time the user clicks Copy (see `CopyButtonProps.onCopy`). |
| `labels` | `Partial<CopyLabels>` |  | Overrides of the built-in texts ("Copy", "Copied", and for a `SecretField` "Reveal" / "Hide"), to translate or reword them. See `CopyLabels`; unset entries keep their English default. |

### SecretField

`SecretField` has the props of `CopyField` and the props of the reveal state.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  | The value shown in the field and written to the clipboard. |
| `id` | `string` |  | Id of the input, so a `<label htmlFor>` (or a FormRow `htmlFor`) can point at it. |
| `mono` | `boolean` | `true` | Monospace value (default `true`): identifiers, URLs, keys. Set `false` for plain words. |
| `what` | `string` |  | What is being copied, used in the copy button's accessible name ("Copy project ID"). |
| `size` | `"sm" \| "md"` | `md` | Field height: `sm` 30px (dense cards, toolbars) or `md` 34px (default, form rows). |
| `className` | `string` |  | Merged onto the wrapper element. |
| `aria-label` | `string` |  | Accessible name of the input when no `<label htmlFor>` points at it. |
| `aria-describedby` | `string` |  | Id(s) of the helper text describing the field. |
| `onCopy` | `((value: string) => void)` |  | Called with `value` each time the user clicks Copy (see `CopyButtonProps.onCopy`). |
| `labels` | `Partial<CopyLabels>` |  | Overrides of the built-in texts ("Copy", "Copied", and for a `SecretField` "Reveal" / "Hide"), to translate or reword them. See `CopyLabels`; unset entries keep their English default. |
| `mask` | `string` |  | Text shown while hidden. Defaults to bullets roughly as long as the value (12–32). |
| `revealed` | `boolean` |  | Controlled reveal state. Pair with `onRevealedChange`. |
| `defaultRevealed` | `boolean` | `false` | Initial reveal state when uncontrolled (default `false`). |
| `onRevealedChange` | `((revealed: boolean) => void)` |  | Called when the user toggles Reveal / Hide. |

### CopyLabels

The type of the `labels` object. Each entry is optional.

| Key | Default | Text |
| --- | --- | --- |
| `copy` | "Copy" | The Copy button of a field. |
| `copied` | "Copied" | The confirmation after a copy. |
| `copyWhat(what)` | "Copy API key" | The name and the tooltip of an icon button. |
| `reveal` | "Reveal" | The tooltip of the reveal button while the mask shows. |
| `hide` | "Hide" | The tooltip of the reveal button while the value shows. |
| `revealWhat(what)` | "Reveal API key" | The name of the reveal button while the mask shows. |
| `hideWhat(what)` | "Hide API key" | The name of the reveal button while the value shows. |
