The definitive guide to the JavaScript Clipboard API

By

Learn the JavaScript Clipboard API for copying and pasting text, images, and rich data, with permissions, user activation, security, and fallbacks.

~~~

The Clipboard API lets a web page copy data to the system clipboard and, with stricter limits, read data back.

The common example is a Copy button beside a command, token, address, or code snippet. The API can also work with images and rich data where the browser supports those formats.

Clipboard access is powerful. A page could replace a copied command or read a password that the user copied elsewhere. Browsers therefore require a secure context, focus, permission, user activation, or a combination of those controls.

Start with text. It is the simplest and most widely useful case.

Copy text with writeText()

Add a button:

<pre><code id="command">npm run build</code></pre>
<button id="copy" type="button">Copy command</button>
<span id="copy-status" aria-live="polite"></span>

Call navigator.clipboard.writeText() from the click handler:

const button = document.querySelector('#copy')
const command = document.querySelector('#command')
const status = document.querySelector('#copy-status')

button.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText(command.textContent)
    status.textContent = 'Copied'
  } catch (error) {
    status.textContent = 'Could not copy the command'
  }
})

The method returns a promise. Show success only after it resolves.

The live region announces the result without moving keyboard focus. Keep the original text selectable so the user can copy it manually if the API fails.

Clipboard access needs the right context

The asynchronous Clipboard API is available only in a secure context. Use HTTPS in production. Browsers normally treat localhost as trustworthy during development.

The document also needs focus. Clipboard writes usually need a recent user action such as a click or keyboard activation. Clipboard reads are more restricted and may display a prompt or require a browser-provided paste action.

These details differ between browsers. Do not build your interface around one specific permission prompt.

Feature-detect the method you need:

if (!navigator.clipboard?.writeText) {
  showManualCopyInstructions()
}

Checking for navigator.clipboard alone is not enough when you need read(), write(), or rich formats.

Copy a value from an input

Read the input’s value, not its initial HTML attribute:

<input id="invite-link" value="https://flaviocopes.com">
<button id="copy-link" type="button">Copy link</button>
const input = document.querySelector('#invite-link')
const button = document.querySelector('#copy-link')

button.addEventListener('click', async () => {
  await navigator.clipboard.writeText(input.value)
})

Do not make the text itself the only control. A button clearly communicates the action and works with keyboard navigation.

Read text with readText()

readText() returns the clipboard’s text representation:

<button id="paste" type="button">Paste from clipboard</button>
<textarea id="notes"></textarea>
const pasteButton = document.querySelector('#paste')
const notes = document.querySelector('#notes')

pasteButton.addEventListener('click', async () => {
  try {
    notes.value = await navigator.clipboard.readText()
  } catch (error) {
    notes.focus()
  }
})

The browser may deny the call even after a click. Reading exposes whatever the user last copied, which may contain private data.

If the read fails, focus the normal input and let the user paste with the operating system shortcut. Native paste is an excellent fallback.

Do not poll the clipboard. Read only when the user asks your interface to paste.

Handle the paste event

For an editable field, you often do not need readText(). Listen for the user’s normal paste action:

notes.addEventListener('paste', (event) => {
  const text = event.clipboardData.getData('text/plain')
  console.log(text)
})

The paste event fires before the browser inserts data. It bubbles and can be canceled.

Only cancel it when you need to replace the default insertion:

notes.addEventListener('paste', (event) => {
  event.preventDefault()

  const text = event.clipboardData.getData('text/plain')
  const cleaned = text.replaceAll('\t', '  ')

  notes.setRangeText(
    cleaned,
    notes.selectionStart,
    notes.selectionEnd,
    'end',
  )
})

This approach uses data provided for that paste operation. It does not grant ongoing access to the system clipboard.

Do not block paste in password, payment, or confirmation fields. Preventing password-manager and clipboard use usually reduces both usability and security.

Handle copy and cut events

You can inspect or replace data during a user-initiated copy:

document.addEventListener('copy', (event) => {
  const selection = document.getSelection().toString()

  if (!selection) return

  event.preventDefault()
  event.clipboardData.setData('text/plain', selection)
})

The same event model supports cut.

Use this sparingly. Users expect Copy to copy what they selected. Quietly appending marketing text or replacing a URL breaks that expectation and can be dangerous for commands or addresses.

Synthetic clipboard events do not gain access to the real system clipboard.

Copy images and rich data

Use navigator.clipboard.write() with ClipboardItem:

const response = await fetch('/images/logo.png')
const image = await response.blob()

const item = new ClipboardItem({
  [image.type]: image,
})

await navigator.clipboard.write([item])

The clipboard item maps a MIME type to a Blob or a promise that resolves to one.

Support for formats varies. Check whether the browser can write a particular type:

if (ClipboardItem.supports('image/png')) {
  // offer Copy image
}

PNG is a common interoperable image format. Other image and custom formats need testing in the browsers and operating systems you support.

You can provide plain text and HTML representations of the same content:

const plain = new Blob(
  ['Read the JavaScript guide'],
  { type: 'text/plain' },
)

const html = new Blob(
  ['<strong>Read the JavaScript guide</strong>'],
  { type: 'text/html' },
)

const item = new ClipboardItem({
  'text/plain': plain,
  'text/html': html,
})

await navigator.clipboard.write([item])

The destination chooses the representation it understands. Always include plain text when rich formatting is optional.

Never copy untrusted HTML without deciding what should survive. Rich clipboard content can carry links, styles, images, and markup into another application.

Prepare clipboard data before the click when necessary

User activation can expire while JavaScript waits for slow work. A Copy button should not start a large network request and only then try to write.

Prepare data before enabling the button when possible:

let reportBlob

async function prepareReport() {
  const response = await fetch('/reports/latest.png')
  reportBlob = await response.blob()
  copyButton.disabled = false
}

copyButton.addEventListener('click', async () => {
  const item = new ClipboardItem({
    [reportBlob.type]: reportBlob,
  })

  await navigator.clipboard.write([item])
})

Some browsers support promises as ClipboardItem values, but interoperability and activation behavior still deserve testing. The simplest reliable flow is to have the data ready when the user clicks.

If generating the data takes time, use a two-step interface: Generate, then Copy. This is clearer than leaving a Copy button busy while its permission window disappears.

Clipboard access inside an iframe

An embedded document can be restricted by Permissions Policy. The parent can delegate clipboard features with the iframe allow attribute:

<iframe
  src="https://editor.example.org"
  allow="clipboard-read; clipboard-write"
></iframe>

Delegation does not bypass browser permission, focus, or user-activation rules. It only allows the embedded origin to reach the relevant policy-controlled feature.

Grant clipboard access only to an iframe origin you trust. If the embedded editor only needs native paste into a text field, it may not need asynchronous clipboard reads at all.

Copy generated URLs correctly

When copying a URL assembled by the page, use the URL API instead of string concatenation:

const url = new URL('/invite', location.origin)
url.searchParams.set('code', inviteCode)

await navigator.clipboard.writeText(url.href)

This handles encoding and produces an absolute URL. Never include a secret in a copied URL unless the user understands that anyone with the URL can use it.

Read images and rich data

Use navigator.clipboard.read():

const items = await navigator.clipboard.read()

for (const item of items) {
  if (!item.types.includes('image/png')) continue

  const blob = await item.getType('image/png')
  const url = URL.createObjectURL(blob)

  showImagePreview(url)
}

Revoke object URLs when the preview no longer needs them:

URL.revokeObjectURL(url)

Inspect item.types rather than assuming a format exists. The operating system and browser may convert formats, remove unsupported representations, or expose only a safe subset.

Reading rich data is not a substitute for validation. Treat pasted files and markup as untrusted input.

Inspect pasted files without reading the whole clipboard

The paste event can expose files supplied by the user’s paste action:

editor.addEventListener('paste', (event) => {
  for (const item of event.clipboardData.items) {
    if (item.kind !== 'file') continue

    const file = item.getAsFile()
    if (file) showFilePreview(file)
  }
})

This is useful for an editor that accepts screenshots. Keep the normal paste behavior for text, and cancel the event only when your code fully handles the selected file.

Validate the file type and size before previewing or uploading it. The server must validate it again.

There is no portable clear clipboard method

The Clipboard API does not provide a dedicated clear() method.

Writing an empty string is not the same as clearing every representation:

await navigator.clipboard.writeText('')

It replaces the text representation with an empty value where the browser allows the write. Do not promise that this securely erases clipboard history maintained by the operating system or another application.

If your interface copies a temporary secret, tell the user that the system clipboard is outside the page’s control. Prefer short-lived tokens and server-side expiration over attempts to clear the clipboard later.

Likewise, a web page cannot reliably monitor every clipboard change. Design around explicit Copy and Paste actions instead of treating the clipboard as synchronized application storage.

Query permissions carefully

The Clipboard specification integrates clipboard writes with the Permissions API, but implementations differ.

Where supported, you can query the write state:

const permission = await navigator.permissions.query({
  name: 'clipboard-write',
})

console.log(permission.state)

Do not make this query a prerequisite. Some browsers do not expose the same descriptor, and a state does not remove user-activation or focus requirements.

Call the clipboard method after the user asks for it and handle rejection. That is the reliable application flow.

Provide a copy fallback

document.execCommand('copy') is deprecated, but it remains a possible compatibility fallback for older environments.

Use it only when the modern API is missing:

function legacyCopy(text) {
  const textarea = document.createElement('textarea')
  textarea.value = text
  textarea.setAttribute('readonly', '')
  textarea.style.position = 'fixed'
  textarea.style.opacity = '0'

  document.body.append(textarea)
  textarea.select()

  const copied = document.execCommand('copy')
  textarea.remove()

  return copied
}

This fallback has limitations and can disturb selection or focus. Preserve the previously focused element if you use it.

The final fallback is simpler: show the text in a selectable field and explain the normal copy shortcut.

Keep the user informed

A good Copy button has visible states:

Avoid changing the button width when its label changes. Do not show “Copied” before the promise resolves.

Reset temporary status after a short delay:

let statusTimer

async function copyText(text) {
  clearTimeout(statusTimer)

  try {
    await navigator.clipboard.writeText(text)
    status.textContent = 'Copied'

    statusTimer = setTimeout(() => {
      status.textContent = ''
    }, 2000)
  } catch (error) {
    status.textContent = 'Select the text and copy it manually'
  }
}

If copying is not available, do not leave a dead button in the interface.

Clipboard security rules

Clipboard code deserves a security review because it crosses application boundaries.

Follow these rules:

A clipboard can contain a password, private message, API token, or shell command. Access only the representation needed for the task.

When I would use it

I would use writeText() for commands, generated links, IDs, and small code snippets. It removes a fiddly selection step and has a clear user action.

I would use readText() much less often. A dedicated Paste button can make sense in a code editor or import tool, but a normal text field already gives the user a familiar and private paste flow.

I would use rich clipboard writes only when copying an image or formatted content is the feature itself. Plain text should remain available as the fallback.

The W3C Clipboard API and Events specification defines the asynchronous methods, clipboard events, permissions model, and security considerations.

~~~

Related posts about platform: