# Copying text into the system’s clipboard with JavaScript

The Clipboard API makes copying text from a React app straightforward. Use the
older `execCommand()` method only when you need a legacy fallback.

## Copy text with the Clipboard API

`navigator.clipboard.writeText()` accepts a string and returns a promise. Call
it from a user action, such as a button click:

```tsx {11}
import { useState } from 'react'

const text = 'Here is some text to copy.'

/** Renders a button that copies text to the clipboard */
export default function CopyApp() {
  const [copyMessage, setCopyMessage] = useState('')

  async function handleCopy() {
    try {
      await navigator.clipboard.writeText(text)
      setCopyMessage('Text copied')
    } catch {
      setCopyMessage('Unable to copy')
    }
  }

  return (
    <div className='app'>
      <p>{text}</p>
      <button type='button' onClick={handleCopy}>
        Copy text
      </button>
      <span aria-live='polite'>{copyMessage}</span>
    </div>
  )
}
```

The API works only in a
[secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts),
usually HTTPS or localhost. Browser permission rules differ, but calling it
directly from a click handler is the safest approach. In Chromium, a
cross-origin iframe also needs `clipboard-write` allowed by its embedding page's
Permissions Policy.

The `Clipboard` interface has four main methods:

- `writeText()` writes a string
- `write()` writes data such as images or HTML
- `readText()` reads a string
- `read()` reads arbitrary clipboard data

Reading is more restricted than writing. Both can fail, so always handle the
rejected promise.

## Legacy fallback: execCommand

`document.execCommand('copy')` is deprecated and may disappear. It copies the
current selection, so the usual flow is:

1. Focus an input or textarea
2. Select its text
3. Run the `copy` command
4. Clear the selection

Here is the same UI using the old method:

```tsx {12-13,18,23-25}
import { useRef, useState } from 'react'

/** Renders a legacy clipboard-copy example */
export default function CopyApp() {
  const textareaRef = useRef<HTMLTextAreaElement>(null)
  const [copyMessage, setCopyMessage] = useState('')

  function handleCopy() {
    const textarea = textareaRef.current
    if (textarea === null) return

    textarea.focus()
    textarea.select()

    let wasCopied = false

    try {
      wasCopied = document.execCommand('copy')
    } catch {
      // Some browsers throw when the command is unavailable
    }

    setCopyMessage(wasCopied ? 'Text copied' : 'Unable to copy')
    textarea.setSelectionRange(0, 0)
    textarea.blur()
  }

  return (
    <div className='app'>
      <textarea defaultValue='Here is some text to copy.' ref={textareaRef} />
      <button type='button' onClick={handleCopy}>
        Copy text
      </button>
      <span aria-live='polite'>{copyMessage}</span>
    </div>
  )
}
```

The selection does not have to come from an input or textarea: a DOM
[`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) can select
text in another element. Inputs are simply easier to select.

Do not use `<textarea hidden>` for this fallback. It is not rendered, so it
cannot be focused and selected like a normal control. To copy an arbitrary
string, create an off-screen textarea, append it to the document, select it,
copy, then remove it.

## Which method should you use?

Use `navigator.clipboard.writeText()` for new code. Keep `execCommand('copy')`
only when you have tested a real need for the deprecated fallback.
