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:
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,
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 stringwrite()writes data such as images or HTMLreadText()reads a stringread()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:
- Focus an input or textarea
- Select its text
- Run the
copycommand - Clear the selection
Here is the same UI using the old method:
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 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.