useCopyToClipboard Hook for React
A typed React hook that copies any string to the clipboard with a success state, error handling, and an automatic reset timer.
useCopyToClipboard is a React hook that copies a string and gives you a copied boolean for showing UI feedback. It auto-resets after a configurable delay so your copy button can flip back to its idle state without extra logic.
Tested on React 19, modern browsers (HTTPS or localhost only).
When to Use This
- Copy buttons next to code blocks, share URLs, or invite codes
- One-click copy for API keys or generated tokens
- Any UI element where the user expects "Copy" to flip to "Copied!" briefly
Don't use this when you're targeting old browsers without navigator.clipboard support — fall back to the deprecated document.execCommand('copy') or a library like clipboard.js.
Code
import { useCallback, useEffect, useState } from 'react';
interface UseCopyToClipboardResult {
copied: boolean;
error: Error | null;
copy: (text: string) => Promise<void>;
}
export function useCopyToClipboard(resetMs = 2000): UseCopyToClipboardResult {
const [copied, setCopied] = useState(false);
const [error, setError] = useState<Error | null>(null);
const copy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setError(null);
} catch (err) {
setCopied(false);
setError(err instanceof Error ? err : new Error('Clipboard write failed'));
}
}, []);
useEffect(() => {
if (!copied) return;
const timer = setTimeout(() => setCopied(false), resetMs);
return () => clearTimeout(timer);
}, [copied, resetMs]);
return { copied, error, copy };
}The useEffect resets copied after the configured delay. The useCallback keeps the copy reference stable across renders so consumers can pass it to memoized children without re-renders.
Usage
A copy button next to a generated API key:
'use client';
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
import { Check, Copy } from 'lucide-react';
export function ApiKeyDisplay({ apiKey }: { apiKey: string }) {
const { copied, copy } = useCopyToClipboard();
return (
<div className="flex items-center gap-2">
<code className="font-mono text-sm">{apiKey}</code>
<button onClick={() => copy(apiKey)} aria-label="Copy API key">
{copied ? <Check className="text-emerald-500" /> : <Copy />}
</button>
</div>
);
}The button icon flips for 2 seconds after a successful copy, then resets automatically.
Things to Know
- HTTPS or localhost only.
navigator.clipboardis gated behind a secure context. Onhttp://192.168.x.x, it throws. The error state covers this — show a tooltip iferroris set. - The user must trigger the call. Some browsers require a user gesture (click, key press) for clipboard writes. Calling
copy()from auseEffecton mount fails silently in Safari. - Don't store secrets in component state for too long. If you copy an API key, the value sits in component state for the duration of the tooltip. Pass the secret in directly from props instead.
writeTextonly handles strings. For images or rich content usenavigator.clipboard.writewith aClipboardItem. That's a different hook.
Related Snippets & Reading
- useDebounce Hook for React — another foundational React utility
- useKeyboardShortcut Hook(coming soon) — bind Cmd+C / Ctrl+C to a copy action
- MDN Clipboard API — secure-context rules and full reference
Frequently Asked Questions
Why not just call navigator.clipboard.writeText directly?
You can, but you lose the success/error state and the automatic reset that turns 'Copy' into 'Copied!' for two seconds. This hook wraps that pattern so every copy button in your app behaves consistently with one line of code.
Does navigator.clipboard work in every browser?
It works in every modern browser, but only over HTTPS or on localhost. On insecure origins it throws. The hook catches that and exposes an error state so you can show a fallback UI or a 'copy unavailable' message.