Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useCallback, useRef } from "react";
interface UseCopyToClipboardReturn {
/** The most recently copied text, or null */
copiedText: string | null;
/** Whether text was recently copied (resets after timeout) */
isCopied: boolean;
/** Copy the given text to the clipboard */
copy: (text: string) => Promise<boolean>;
/** Reset the copied state manually */
reset: () => void;
}
function useCopyToClipboard(
resetDelay: number = 2000
): UseCopyToClipboardReturn {
const [copiedText, setCopiedText] = useState<string | null>(null);
const [isCopied, setIsCopied] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const reset = useCallback(() => {
setCopiedText(null);
setIsCopied(false);
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const copy = useCallback(
async (text: string): Promise<boolean> => {
// Try the modern Clipboard API first
if (navigator?.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
setCopiedText(text);
setIsCopied(true);
// Auto-reset after delay
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setIsCopied(false);
timerRef.current = null;
}, resetDelay);
return true;
} catch {
// Clipboard API failed (e.g., permissions denied)
}
}
// Fallback: execCommand for older browsers
try {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand("copy");
document.body.removeChild(textarea);
if (success) {
setCopiedText(text);
setIsCopied(true);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setIsCopied(false);
timerRef.current = null;
}, resetDelay);
}
return success;
} catch {
return false;
}
},
[resetDelay]
);
return { copiedText, isCopied, copy, reset };
}When to reach for this: You have a "Copy" button next to code snippets, API keys, URLs, or share links and want visual feedback when the copy succeeds.
"use client";
function CodeBlock({ code }: { code: string }) {
const { isCopied, copy } = useCopyToClipboard(3000);
return (
<div style={{ position: "relative", background: "#1e1e1e", padding: 16, borderRadius: 8 }}>
<pre style={{ color: "#d4d4d4", margin: 0 }}>
<code>{code}</code>
</pre>
<button
onClick={() => copy(code)}
style={{
position: "absolute",
top: 8,
right: 8,
padding: "4px 12px",
background: isCopied ? "#22c55e" : "#3b82f6",
color: "#fff",
border: "none",
borderRadius: 4,
cursor: "pointer",
transition: "background 0.2s",
}}
>
{isCopied ? "Copied!" : "Copy"}
</button>
</div>
);
}
function ShareLink({ url }: { url: string }) {
const { isCopied, copy } = useCopyToClipboard();
return (
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input value={url} readOnly style={{ flex: 1, padding: 8 }} />
<button onClick={() => copy(url)}>
{isCopied ? "Link copied!" : "Share"}
</button>
</div>
);
}What this demonstrates:
isCopied auto-resets after 3 seconds (or the default 2 seconds)copy function returns a boolean for programmatic success checksdocument.execCommand for browsers without the Clipboard APInavigator.clipboard.writeText is the modern, promise-based API. It requires a secure context (HTTPS) and may prompt for permission.textarea, selects its content, and executes the copy command. This is deprecated but widely supported.isCopied flips to true and automatically resets after resetDelay ms. This drives the "Copied!" feedback without manual cleanup.| Parameter | Type | Default | Description |
|---|---|---|---|
resetDelay | number | 2000 | Milliseconds before isCopied resets to false |
| Return | Type | Description |
|---|---|---|
copiedText | string or null | The last successfully copied text |
isCopied | boolean | Whether a recent copy succeeded (auto-resets) |
copy | (text: string) => Promise<boolean> | Trigger copy, returns success |
reset | () => void | Manually reset state |
Copy rich text (HTML): Use navigator.clipboard.write with a ClipboardItem for formatted content:
const blob = new Blob([htmlString], { type: "text/html" });
const item = new ClipboardItem({ "text/html": blob });
await navigator.clipboard.write([item]);Copy from element: Accept a ref instead of a string and read innerText:
const copyFromRef = async (ref: React.RefObject<HTMLElement>) => {
const text = ref.current?.innerText ?? "";
return copy(text);
};copy returns Promise<boolean> so callers can await and react to failure.copy from an event handler, not from a timer or effect.false.document.execCommand("copy") is deprecated and may be removed. Fix: The Clipboard API is the primary path; the fallback is a safety net for legacy browsers.| Package | Hook Name | Notes |
|---|---|---|
usehooks-ts | useCopyToClipboard | Similar API, no fallback |
@uidotdev/usehooks | useCopyToClipboard | Minimal, Clipboard API only |
react-use | useCopyToClipboard | Includes error state |
copy-to-clipboard (npm) | copy() | Not a hook; utility function with fallback |
navigator.clipboard.writeText is the modern, promise-based API and is the preferred approach.document.execCommand("copy") is deprecated but has wider support in older browsers.isCopied flips to true immediately after a successful copy.false after resetDelay milliseconds (default: 2000).Yes. copy returns Promise<boolean>:
const success = await copy("some text");
if (!success) {
showErrorToast("Copy failed");
}reset() manually clears copiedText, sets isCopied to false, and cancels any pending auto-reset timer. Use it when you need to reset state before the auto-reset fires (e.g., when closing a modal).
The Clipboard API requires a secure context (HTTPS or localhost). On plain HTTP, navigator.clipboard is undefined. The hook falls back to execCommand, but test both paths to be sure.
Browsers require clipboard access to happen in response to a user gesture (click, keypress). Calling copy() from a timer, effect, or async callback without a preceding user action will be blocked. Always trigger it from an event handler.
<textarea> is created and appended to the DOM.select() and execCommand("copy") are called.Use navigator.clipboard.write with a ClipboardItem:
const blob = new Blob([htmlString], { type: "text/html" });
const item = new ClipboardItem({ "text/html": blob });
await navigator.clipboard.write([item]);The named UseCopyToClipboardReturn interface provides clear documentation, IDE autocompletion, and can be reused if consumers need to type props that accept the hook's return value.
copy is typed as (text: string) => Promise<boolean>. The input is always a string and the return indicates success or failure. No generics are needed.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥