Source: https://monarchui.dev/components/copy-button/

Components · Actions

# CopyButton

Copies a value to the clipboard, confirms it visibly and to screen readers, and reports failures.

@rookcom/monarch-react

Component

## Labels and icon-only

## Usage examples

### Reporting a failure

### useCopyToClipboard

## Purpose

One button that copies a value (a link, a key, a DNS record), shows a short "Copied" state, announces the result in a polite live region and hands errors to the caller. `useCopyToClipboard` is the same logic for custom controls.

### Use when

- The user needs a value elsewhere: an invite link, a webhook URL, an ID, a code snippet.
- You are building your own copy control: use `useCopyToClipboard` so the success, error and reset behaviour match.

### Avoid when

- The value should also be visible and selectable: use CopyField.
- The value is a secret: use SecretField with `copyable`, which copies without revealing it.

## Design principles

- Feedback is visible and spoken: the icon becomes a check and the label "Copied", and a polite live region says "Copied to clipboard".
- Never fails silently. The Clipboard API is tried first, then the legacy `execCommand` path; if both fail the button shows "Couldn't copy", announces it, and calls `onCopyError`.
- The button keeps its width: all three labels share one grid cell, so nothing jumps when the state changes.
- Built on Button, so variants, sizes, focus and disabled behaviour are the same as every other action.

## API reference

```tsx
import { CopyButton, useCopyToClipboard, writeToClipboard } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `CopyButton` | component | client | `.mn-copy-button` |
| `useCopyToClipboard` | hook | client | – |
| `writeToClipboard` | utility | client | – |

CopyButton forwards its ref to the `<button>`. It renders a visually hidden `role="status"` sibling for announcements.

`useCopyToClipboard({ resetAfter, onCopy, onError })` returns `{ copy, status, error, reset }`. `copy(value)` resolves `true` or `false` and never rejects.

`writeToClipboard(text)` is the low-level write (Clipboard API, then the legacy fallback); it rejects with an Error when both fail.

Generated from the TypeScript declarations of `packages/react/src/components/actions/copy-button`.

## Properties

All Button props except `children`, `asChild` and `leadingIcon` pass through, including `variant` and `size`.

### `CopyButton`

Copies a value to the clipboard, shows a short "Copied" state and announces the result through a polite live region. Errors are reported to the caller through `onCopyError`; the button never fails silently.

| Prop | Type | Description |
| --- | --- | --- |
| `value` required | `CopyValue` | The text to copy, or a function that produces it when clicked. |
| `block` | `boolean` | Stretch to the container width. |
| `brandColor` | `string` | Brand or tenant colour for `variant="brand"`, as `#rrggbb` or `#rgb`. The fill and text are derived per theme by the tokens contrast engine, so the label always meets WCAG AA (7:1 in more-contrast themes); the colour may be darkened or lightened to get there. Ignored by other variants. An invalid or missing colour falls back to the product accent. |
| `className` | `string` |  |
| `copiedLabel` | `string` | Label while in the copied state. Default "Copied". |
| `copiedMessage` | `string` | Polite announcement after a successful copy. Default "Copied to clipboard". |
| `disabled` | `boolean` |  |
| `errorLabel` | `string` | Label after a failed copy. Default "Couldn't copy". |
| `errorMessage` | `string` | Polite announcement after a failure. Default "Couldn't copy. Select the text and copy it manually." |
| `icon` | `ReactNode` | Replaces the copy icon. Decorative. |
| `iconOnly` | `boolean` | Hide the visible text and show only the icon. `label` stays the accessible name, so give it the object: "Copy API key". |
| `id` | `string` |  |
| `label` | `string` | Visible label, and the accessible name. Default "Copy". |
| `loading` | `boolean` | Shows a spinner, sets aria-busy and blocks clicks while keeping focus. |
| `onCopy` | `((text: string) => void)` | Called with the copied text after a successful copy. |
| `onCopyError` | `((error: Error) => void)` | Called when copying fails. Use it to show a toast or a manual-copy fallback. |
| `resetAfter` | `number` | Milliseconds before the button returns to its idle look. Default 2000. |
| `size` | `ButtonSize` |  |
| `target` | `string` | What is copied, for screen readers ("webhook URL"). Appended to the visible label as visually hidden text, so several copy buttons on one page have distinct names: "Copy webhook URL", "Copied webhook URL". |
| `trailingIcon` | `ReactNode` |  |
| `variant` | `ButtonVariant` | Visual weight. Use one `primary` per view region. Default `secondary`. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `variant` | `'primary'``'secondary'``'ghost'``'danger'``'danger-subtle'` | From Button. Default `secondary`, or `ghost` when `iconOnly`. |
| `size` | `'xs'``'sm'``'md'``'lg'` | From Button. Default `md`. |
| `iconOnly` | `'true'``'false'` | Square, icon-only button (`data-icon-only`). `label` stays the accessible name and tooltip. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Idle | `data-state="idle"` | Copy icon and `label`. |
| Copied | `data-state="copied"` | Check icon in the success colour and `copiedLabel`, for `resetAfter` milliseconds (default 2000). |
| Error | `data-state="error"` | Warning icon in the danger colour and `errorLabel`; `onCopyError` has been called. |
| Disabled | `disabled` | From Button: nothing is copied. |

## Interaction behaviour

- Click, Enter or Space copies `value`. A function `value` is called at that moment and may be async.
- Your `onClick` runs first; call `event.preventDefault()` in it to skip the copy.
- Each attempt clears and refills the live region, so copying twice is announced twice.
- The state returns to idle after `resetAfter`; `0` keeps it until the next attempt.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves focus to the button. |
| Enter / Space | Copies the value. |

## Accessibility

Role

button

Accessible name

The visible label, plus the visually hidden

target

("Copy" + "invite link"). Icon-only:

label

(+

target

) as

aria-label

.

- The result is announced through a `role="status"` live region outside the button, so it is not part of the button name.
- Icon-only buttons keep a stable `aria-label`; the change is announced instead of renamed.
- Use `target` when several copy buttons share a screen, so each has a distinct name (WCAG 2.4.6). The visible text stays at the start of the name (WCAG 2.5.3).
- Icons are decorative (`aria-hidden`).

## Responsive behaviour

- Same height as Button at every width (`--mn-control-*`).
- The label never wraps; use `iconOnly` in tight rows such as table cells.

## Theme support

- Colours come from Button and the success and danger tokens, so light, dark and more-contrast themes apply automatically.
- On `primary` and `danger` buttons the state icon inherits the on-solid text colour.
- The icon colour transition is removed under `prefers-reduced-motion: reduce`.

## Do and don't

### Do

- Name what is copied: `target="API key"` or `label="Copy invite link"`.
- Handle `onCopyError`: show a toast or point the user at a selectable copy of the value.
- Use CopyField when the user should also see the value.

### Don't

- Do not show a toast as the only confirmation; the button already confirms visibly and to screen readers.
- Do not put a secret in a plain CopyButton next to its plain text; use SecretField.
- Do not change `label` while the button is showing "Copied"; use `copiedLabel`.

## Related components

- [CopyField](https://monarchui.dev/components/copy-field/): Shows the value, read-only, with this button beside it.
- [SecretField](https://monarchui.dev/components/secret-field/): Copies a masked secret without revealing it.
- [Button](https://monarchui.dev/components/button/): The control CopyButton is built on.
- [Toast](https://monarchui.dev/components/toast/): A product-level way to report `onCopyError`.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/) |

## Testing coverage

- Copies the value, shows the copied state, and announces "Copied to clipboard".
- Copies with Enter and Space.
- A refused write calls `onCopyError`, shows the error state and announces it.
- Falls back to `execCommand` when the Clipboard API rejects; rejects when nothing is available.
- Returns to idle after `resetAfter`.
- Accepts sync and async function values.
- Icon-only keeps a stable accessible name.
- Does not copy when disabled or when `onClick` prevents default.
- Inactive labels are hidden from the accessible name.
- `useCopyToClipboard` exposes status, error and reset.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/copy-button |

Tests: `packages/react/src/components/actions/copy-button`

## All example code

### Labels and icon-only

```tsx
import { CopyButton } from '@rookcom/monarch-react';

<CopyButton value="https://example.com/hooks/inbound" target="webhook URL" />
<CopyButton
  value="https://example.com/s/q3-plan"
  label="Copy link"
  copiedLabel="Link copied"
  variant="primary"
/>
<CopyButton value="ws_01HZX4" label="Copy workspace ID" iconOnly size="sm" />
```

### Reporting a failure

```tsx
import { useState } from 'react';
import { CopyButton } from '@rookcom/monarch-react';

function WithErrorReport() {
  const [problem, setProblem] = useState<string | null>(null);
  return (
    <div style={{ display: 'grid', gap: 'var(--mn-space-2)', justifyItems: 'start' }}>
      <CopyButton
        value="https://example.com/invite/7f3a"
        label="Copy invite link"
        copiedLabel="Link copied"
        onCopy={() => setProblem(null)}
        onCopyError={(error) => setProblem(error.message)}
      />
      {problem ? (
        <p style={{ margin: 0, fontSize: 'var(--mn-text-xs)' }}>
          Copy failed ({problem}). Select the link and copy it manually.
        </p>
      ) : null}
    </div>
  );
}

<WithErrorReport />
```

### useCopyToClipboard

```tsx
import { useCopyToClipboard, Button } from '@rookcom/monarch-react';

function HookDemo() {
  const { copy, status } = useCopyToClipboard();
  return (
    <Button size="sm" onClick={() => void copy('ORDER-2024-0193')}>
      {status === 'copied' ? 'Reference copied' : 'Copy reference'}
    </Button>
  );
}

<HookDemo />
```
