Copy to Clipboard Button (JavaScript and React)

Call navigator.clipboard.writeText(text) from a click handler. It returns a promise, so await it, then show a short Copied message and catch the error case. The API only works on HTTPS or localhost, and it needs a real user action such as a click, so do not call it on page load.

Ways to put data on the clipboard
MethodCopiesStatus
navigator.clipboard.writeText()Plain textBaseline, widely available since March 2020
navigator.clipboard.write([new ClipboardItem(...)])Text, HTML, PNG imagesClipboardItem is Baseline 2024
copy event with event.clipboardData.setData()Changes what a normal Ctrl+C copiesWidely supported
document.execCommand("copy")The current selectionDeprecated, avoid in new code

Updated

A copy button looks trivial, yet the snippets that rank often skip the parts that go wrong: the promise can reject, embedded frames can be blocked, and screen reader users never hear that anything happened. The demo below copies for real in your browser, and the code covers each of those cases.

Try it

Copy buttons for plain text and rich HTML, with a paste box to check the result.

Plain JavaScript

Copy button with feedback and a fallback

The status element is visually hidden but read aloud. On failure the text is selected so the person can press Ctrl+C or Cmd+C.

<input id="share-url" value="https://example.com/invite/42" readonly>
<button type="button" id="copy">Copy link</button>
<span id="copy-status" role="status" class="visually-hidden"></span>

<script>
  const input = document.getElementById("share-url");
  const button = document.getElementById("copy");
  const status = document.getElementById("copy-status");

  button.addEventListener("click", async () => {
    try {
      await navigator.clipboard.writeText(input.value);
      button.textContent = "Copied";
      status.textContent = "Link copied to clipboard";
    } catch {
      input.select();
      status.textContent = "Copy failed. Press Ctrl+C to copy.";
    }
    setTimeout(() => {
      button.textContent = "Copy link";
      status.textContent = "";
    }, 2000);
  });
</script>

React hook

useCopyToClipboard

Returns the state and a copy function. The timer is cleared on unmount so it never updates a removed component.

import { useCallback, useEffect, useRef, useState } from "react";

export function useCopyToClipboard(resetMs = 2000) {
  const [state, setState] = useState("idle"); // "idle" | "copied" | "error"
  const timer = useRef();

  useEffect(() => () => clearTimeout(timer.current), []);

  const copy = useCallback(async (text) => {
    clearTimeout(timer.current);
    try {
      await navigator.clipboard.writeText(text);
      setState("copied");
    } catch {
      setState("error");
    }
    timer.current = setTimeout(() => setState("idle"), resetMs);
  }, [resetMs]);

  return [state, copy];
}

export function CopyButton({ text }) {
  const [state, copy] = useCopyToClipboard();
  return (
    <>
      <button type="button" onClick={() => copy(text)}>
        {state === "copied" ? "Copied" : "Copy"}
      </button>
      <span role="status" className="sr-only">
        {state === "copied" ? "Copied to clipboard" : state === "error" ? "Copy failed" : ""}
      </span>
    </>
  );
}

Rich text, images and the copy event

Copy HTML with a plain text fallback

Each MIME type is a Blob. Paste targets pick the richest format they understand.

async function copyRich(html, text) {
  const item = new ClipboardItem({
    "text/html": new Blob([html], { type: "text/html" }),
    "text/plain": new Blob([text], { type: "text/plain" }),
  });
  await navigator.clipboard.write([item]);
}

async function copyCanvasImage(canvas) {
  const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
  await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
}

Add a source line to normal copies

Use sparingly. Changing what people copy can annoy them, so keep it to quotes or code where a source helps.

document.querySelector("article").addEventListener("copy", (event) => {
  const selected = document.getSelection().toString();
  event.clipboardData.setData("text/plain", selected + "\n\nSource: " + location.href);
  event.preventDefault();
});

Errors and their causes

When copying fails
What you seeLikely causeFix
navigator.clipboard is undefinedPage served over plain HTTPServe over HTTPS, or test on localhost
NotAllowedErrorNo user gesture, or the tab is not focusedCall it inside the click handler
NotAllowedError inside an iframePermissions Policy blocks clipboard-writeAdd allow="clipboard-write" on the iframe
Image copy rejectedUnsupported MIME typeConvert to PNG and check ClipboardItem.supports()

The three rules the browser enforces

Writing to the clipboard is a privileged action, so browsers require a secure page, a focused document and, in Firefox and Safari, a fresh user gesture for each write. Chromium can also grant a persistent clipboard-write permission.

In practice, trigger the copy from a click or key press, do the write early in the handler, and treat failure as normal rather than exceptional.

Feedback people can perceive

Swap the label to Copied for about two seconds and then restore it. A tooltip that appears on hover alone is invisible on touch screens and to keyboard users.

Put the same message in a polite live region. role="status" announces it without interrupting whatever the screen reader was reading.

Rich copies and custom copy events

A ClipboardItem can hold a text/plain and a text/html version at once. Apps that understand HTML paste the formatted one, plain text fields take the other.

To change what a normal Ctrl+C copies, such as adding a source line, listen for the copy event, call event.preventDefault() and set your own data with event.clipboardData.setData().

How it works

  1. Press a Copy button in the demo. The label switches to Copied for two seconds, and a hidden status message announces it.
  2. Paste into the test box to check exactly what landed on your clipboard.
  3. Try the rich copy button, then paste into an email or document editor to see the HTML version keep its formatting.
  4. Copy the plain JavaScript, the React hook or the rich copy code below.

Tips

Frequently asked questions

Why is navigator.clipboard undefined?

The page is not in a secure context. The Clipboard API is only exposed on HTTPS pages and on localhost, so a site served over plain HTTP or from a local network address will not have it.

Why does writeText throw NotAllowedError?

The browser refused the write, usually because there was no recent user action, the page was not focused, or a frame was blocked by Permissions Policy. Call it directly inside a click handler and avoid long awaits before it.

Do I need a library for copy to clipboard in React?

No. A ten line hook around navigator.clipboard.writeText covers it. Libraries mostly add an execCommand fallback for very old browsers, which current browsers do not need.

Can I copy an image to the clipboard?

Yes, with navigator.clipboard.write and a ClipboardItem whose key is image/png and whose value is a PNG Blob. Use ClipboardItem.supports() to check a type first; PNG is the format browsers most reliably accept.

Is document.execCommand('copy') still OK?

It is deprecated. It still works in many browsers under varying conditions, but new code should use the async Clipboard API, falling back to asking the person to copy manually.

Related: JavaScript event listeners, CSS button generator, Website accessibility checklist.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap