React Copy to Clipboard: 2026 Guide + Hook
How to copy to clipboard in React the right way — the modern Clipboard API, why it silently fails on HTTP, and a reusable useCopyToClipboard hook you can drop into any component.
The short answer: use navigator.clipboard.writeText(text) inside a click handler. That's the whole API call. Everything past that point — the HTTPS requirement that breaks it silently in dev, the deprecated fallback still floating around in old answers, and building it into something reusable — is where people actually get stuck, so that's what the rest of this covers.
The Modern Way: navigator.clipboard
The Clipboard API is the current, correct way to do this. It's promise-based, asynchronous, and doesn't require any library — it's built directly into the browser.
function CopyButton({ text }: { text: string }) {
const handleCopy = () => {
navigator.clipboard.writeText(text);
};
return <button onClick={handleCopy}>Copy</button>;
}That's a complete, working implementation for the simplest case. writeText() returns a promise, so in real code you'll want to handle the rejection case too — the copy can fail even when the API exists, usually due to permissions.
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error("Copy failed:", err);
}
};Why It Silently Fails: The HTTPS Requirement
This is the single most common reason "react copy to clipboard" doesn't work the first time someone tries it: navigator.clipboard only exists in secure contexts — HTTPS, or localhost during development. Deploy to an HTTP-only staging URL, or preview inside certain sandboxed environments, and navigator.clipboard itself can be undefined, throwing a TypeError before you even get to the permissions question.
It's worth checking for this explicitly rather than assuming the API is always present, especially if your app might ever be embedded in an iframe or previewed through a tool that doesn't grant clipboard permissions by default.
const canUseClipboard =
typeof navigator !== "undefined" &&
!!navigator.clipboard &&
window.isSecureContext;The Old Way: document.execCommand — and Why You'll Still See It
If you search for "copy to clipboard react" and land on an older Stack Overflow answer or an outdated tutorial, you'll likely run into this pattern instead: create a hidden textarea, put the text in it, select it, then call document.execCommand('copy').
// The legacy fallback — works, but execCommand is deprecated
function legacyCopy(text: string) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}This still works in most browsers today, which is exactly why it keeps getting copy-pasted into new code. But execCommand is a deprecated API — it's not guaranteed to keep working, and it shouldn't be your primary approach going forward. The only reason to keep it around at all is as a fallback for the rare case where navigator.clipboard genuinely isn't available.
Building a Reusable useCopyToClipboard Hook
Copying text is one line. What actually needs to be reusable is everything around it — tracking whether the copy just succeeded, resetting that state after a couple of seconds, and handling the error case consistently, instead of rewriting that logic in every component that needs a copy button.
import { useState, useCallback, useRef, useEffect } from "react";
function useCopyToClipboard(resetDelay = 2000) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const copy = useCallback(
async (text: string) => {
if (!navigator.clipboard) {
setCopied(false);
return false;
}
try {
await navigator.clipboard.writeText(text);
setCopied(true);
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setCopied(false), resetDelay);
return true;
} catch {
setCopied(false);
return false;
}
},
[resetDelay],
);
// clean up the timeout if the component unmounts mid-reset
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
return { copied, copy };
}Notice the cleanup effect at the bottom — if the component using this hook unmounts before the reset timeout fires, that's a real memory-leak-adjacent bug (React will warn about setting state on an unmounted component). It's a small detail, but it's exactly the kind of edge case that separates a demo from something you'd actually ship.
Using the Hook: A "Copied!" Button
With the hook written, the component itself gets simple — it just reads copied and calls copy():
function CopyButton({ text }: { text: string }) {
const { copied, copy } = useCopyToClipboard();
return (
<button onClick={() => copy(text)}>
{copied ? "Copied!" : "Copy"}
</button>
);
}This is the pattern almost everyone searching for this is actually trying to build — a button that briefly confirms the copy happened, then reverts on its own. Because the state-and-timeout logic lives in the hook, every copy button in your app can share it without duplicating the reset logic.
Common Mistakes
- Calling it outside a user gesture — triggering the copy from a useEffect, a timer, or right after an async response (rather than directly inside a click handler) can be blocked by the browser's permission model, even on HTTPS.
- Not handling the rejected promise — writeText() can fail even when the API exists, most often due to permissions. An uncaught rejection here fails silently from the user's perspective, with no visible error and no successful copy.
- Forgetting to clear the reset timeout on unmount — if the component using the hook unmounts before the "Copied!" state resets, you'll get a React warning about setting state on an unmounted component, and in a class-based or non-hook implementation, a real memory leak.
- Assuming navigator.clipboard always exists — it's undefined outside secure contexts and in some sandboxed iframes. Checking for it before calling it avoids a hard crash in those environments.
- Relying only on the deprecated execCommand path in new code — it happens to still work in most browsers today, but it's not the API to build new features around going forward.
"The copy call is one line. Everything that actually breaks in production is the surrounding edge case — the secure-context check, the user-gesture requirement, the cleanup on unmount."
The Practical Takeaway
For copy-to-clipboard in React specifically: use navigator.clipboard.writeText() as your primary method, call it directly inside a user-triggered event handler, and wrap it in a small hook that handles the copied-state reset and the failure case once, instead of rewriting that logic in every component. Keep the execCommand fallback only if you genuinely need to support environments where the modern API isn't available — don't lead with it in new code.
Want to build this exact component yourself and confirm it holds up against real tests — including the unmount and error-handling edge cases covered above? Try the Copy to Clipboard challenge on ReactGrind →
Frequently asked questions
How do I copy text to clipboard in React?
Use the modern Clipboard API: navigator.clipboard.writeText(text). Call it from inside a user-triggered event handler, like an onClick, since browsers require a real user gesture for clipboard write access — calling it on page load or inside a useEffect will silently fail or throw a permissions error.
Why isn't navigator.clipboard working in my React app?
The most common cause is running on an insecure origin — navigator.clipboard only works on HTTPS or localhost. It's also not available inside some sandboxed iframes (like certain embedded previews), and it requires a direct user gesture, so calling it asynchronously after an unrelated delay can fail even on HTTPS.
Is navigator.clipboard supported in all browsers?
It's supported in all current major browsers — Chrome, Firefox, Safari, and Edge — but only in secure contexts (HTTPS or localhost). For older browsers or non-secure contexts, you need a fallback using the deprecated document.execCommand('copy'), though that path is being phased out and shouldn't be your primary method in new code.
How do I show a 'Copied!' message after copying text in React?
Track a boolean or timestamp in state that flips to true right after a successful copy, conditionally render your 'Copied!' text or icon based on it, and use a setTimeout to reset it back to false after a couple of seconds. Wrapping this pattern in a custom hook keeps it reusable across every copy button in your app.
What is the copy to clipboard React hook?
A useCopyToClipboard hook is a custom React hook that wraps the Clipboard API, exposing a copy function plus a piece of state indicating whether the copy just succeeded. It centralizes the try/catch error handling, the secure-context check, and the copied-state reset logic so individual components don't have to repeat that logic each time.