BEGINNERREACTHOOKS

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.

Published Apr 18, 2026
reacthooksclipboardtypescriptui

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.clipboard is gated behind a secure context. On http://192.168.x.x, it throws. The error state covers this — show a tooltip if error is set.
  • The user must trigger the call. Some browsers require a user gesture (click, key press) for clipboard writes. Calling copy() from a useEffect on 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.
  • writeText only handles strings. For images or rich content use navigator.clipboard.write with a ClipboardItem. That's a different hook.

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.

X (Twitter)LinkedIn