Open Hooks
Event Handling

useClipboard

Copy text to clipboard with success feedback and error handling in React applications.

useClipboard

Copy text to clipboard with success feedback and error handling in React applications.

Status: stable
Version: 2.0.0
Bundle Size: ~0.8kb
Requires: React 16.8+

Quick Copy

Copy your text with a single click.

Installation

npx open-hook add useClipboard
yarn dlx open-hook add useClipboard
pnpm dlx open-hook add useClipboard

API Reference

Parameters

PropTypeDefault
resetTime?
number
2000

Returns

PropTypeDefault
clearCopied?
() => void
-
copyToClipboard?
(text: string) => Promise<boolean>
-
isCopied?
boolean
-

TypeScript Signature

interface UseClipboardReturn {
  isCopied: boolean;
  copyToClipboard: (text: string) => Promise<boolean>;
  clearCopied: () => void;
}

function useClipboard(resetTime?: number): UseClipboardReturn;

Advanced Usage Examples

function CodeBlock({ code, language }: { code: string; language: string }) {
  const { isCopied, copyToClipboard } = useClipboard(3000);

  const handleCopy = async () => {
    const success = await copyToClipboard(code);
    if (success) {
      // Optional: analytics tracking
      trackEvent("code_copied", { language });
    }
  };

  return (
    <div className="relative">
      <pre className="code-block">
        <code className={`language-${language}`}>{code}</code>
      </pre>

      <button
        onClick={handleCopy}
        className="absolute top-2 right-2 copy-button"
        aria-label={isCopied ? "Copied!" : "Copy code"}
      >
        {isCopied ? (
          <>
            <CheckIcon /> Copied!
          </>
        ) : (
          <>
            <CopyIcon /> Copy
          </>
        )}
      </button>
    </div>
  );
}
function ShareComponent({ title, url }: { title: string; url: string }) {
  const { isCopied, copyToClipboard } = useClipboard();

  const shareData = {
    title,
    url,
    text: `Check out: ${title}`,
  };

  const handleCopyLink = () => {
    copyToClipboard(url);
  };

  const handleCopyText = () => {
    const shareText = `${title}\n${url}`;
    copyToClipboard(shareText);
  };

  const handleNativeShare = async () => {
    if (navigator.share) {
      try {
        await navigator.share(shareData);
      } catch (err) {
        // Fallback to copy
        handleCopyLink();
      }
    } else {
      handleCopyLink();
    }
  };

  return (
    <div className="share-buttons">
      <button onClick={handleCopyLink}>
        {isCopied ? "Link Copied!" : "Copy Link"}
      </button>

      <button onClick={handleCopyText}>Copy Full Text</button>

      <button onClick={handleNativeShare}>Share</button>
    </div>
  );
}
function FormSummary({ formData }: { formData: any }) {
  const { isCopied, copyToClipboard } = useClipboard();

  const formatFormData = (data: any) => {
    return Object.entries(data)
      .map(([key, value]) => `${key}: ${value}`)
      .join("\n");
  };

  const copyAsText = () => {
    const formatted = formatFormData(formData);
    copyToClipboard(formatted);
  };

  const copyAsJSON = () => {
    const json = JSON.stringify(formData, null, 2);
    copyToClipboard(json);
  };

  const copyAsCsv = () => {
    const headers = Object.keys(formData).join(",");
    const values = Object.values(formData).join(",");
    const csv = `${headers}\n${values}`;
    copyToClipboard(csv);
  };

  return (
    <div className="form-summary">
      <h3>Form Summary</h3>
      <pre>{JSON.stringify(formData, null, 2)}</pre>

      <div className="copy-buttons">
        <button onClick={copyAsText}>
          {isCopied ? "Copied!" : "Copy as Text"}
        </button>
        <button onClick={copyAsJSON}>Copy as JSON</button>
        <button onClick={copyAsCsv}>Copy as CSV</button>
      </div>
    </div>
  );
}
function MultiCopyManager() {
  const clipboard1 = useClipboard(1500);
  const clipboard2 = useClipboard(3000);

  const items = [
    { id: 1, label: "API Key", value: "sk-1234567890abcdef" },
    { id: 2, label: "Secret", value: "secret_abcdef1234567890" },
    { id: 3, label: "URL", value: "https://api.example.com/v1" },
  ];

  const copyItem = (value: string, type: "sensitive" | "normal") => {
    if (type === "sensitive") {
      // Shorter feedback time for sensitive data
      clipboard1.copyToClipboard(value);
    } else {
      clipboard2.copyToClipboard(value);
    }
  };

  return (
    <div className="multi-copy">
      {items.map((item) => (
        <div key={item.id} className="copy-item">
          <label>{item.label}:</label>
          <code className="value">
            {item.label.includes("Secret") ? "••••••••" : item.value}
          </code>
          <button
            onClick={() =>
              copyItem(
                item.value,
                item.label.includes("Secret") ? "sensitive" : "normal"
              )
            }
          >
            {clipboard1.isCopied || clipboard2.isCopied ? "Copied!" : "Copy"}
          </button>
        </div>
      ))}
    </div>
  );
}

Best Practices

Provide clear feedback and handle errors gracefully:

const { isCopied, copyToClipboard } = useClipboard();

const handleCopy = async () => {
  const success = await copyToClipboard(text);

  if (success) {
    // Show success feedback
    showNotification("Copied to clipboard!");
  } else {
    // Handle failure gracefully
    showNotification("Failed to copy. Please try again.", "error");
  }
};

Security & UX Guidelines

  • 🔒 Sensitive Data: Use shorter reset times for sensitive content
  • 📱 Mobile Support: Include fallbacks for older browsers
  • ♿ Accessibility: Provide proper ARIA labels and announcements
  • 🎨 Visual Feedback: Clear success/failure states

Do's and Don'ts

  • ✅ Provide visual feedback when copying
  • ✅ Use appropriate reset times for different content types
  • ✅ Handle clipboard permission errors gracefully
  • ✅ Include keyboard shortcuts for power users
  • ❌ Don't copy sensitive data without user consent
  • ❌ Avoid copying very large amounts of text
  • ❌ Don't rely solely on clipboard for critical functionality
  • ❌ Avoid copying without clear user intent

Performance Metrics

MetricValueDescription
Bundle Size~0.8kbMinified + gzipped
Copy Speed< 10msModern browsers
Fallback SupportIE 11+execCommand fallback
Memory UsageMinimalSingle state + timeout

Browser Compatibility

BrowserSupportedNotes
Chrome 66+YesFull Clipboard API support
Firefox 63+YesFull Clipboard API support
Safari 13.1+YesFull Clipboard API support
Edge 79+YesFull Clipboard API support
IE 11YesexecCommand fallback
Mobile SafariYesiOS 13.4+ required

Use Cases

Code Snippets

Copy code blocks and documentation examples with one click.

Share Functionality

Copy URLs, text content, or formatted data for sharing.

Form Data Export

Copy form contents in various formats (JSON, CSV, plain text).

API Keys & Tokens

Securely copy sensitive credentials with clear feedback.

Accessibility

Accessibility Considerations

  • Use proper ARIA labels for copy buttons - Announce successful copies to screen readers - Provide keyboard shortcuts (Ctrl+C alternatives) - Include visual and auditory feedback for copy actions - Ensure copy buttons have sufficient contrast and touch targets

Troubleshooting

Internals

How It Works

The hook uses the modern Clipboard API when available, with a fallback to the older document.execCommand('copy') method for broader browser support. It manages the copied state with automatic reset after a specified time and provides both the current state and control functions.

The implementation prioritizes user experience with clear feedback and graceful error handling.

Testing Example

import { renderHook, act } from "@testing-library/react";
import { useClipboard } from "./useClipboard";

// Mock clipboard API
Object.assign(navigator, {
  clipboard: {
    writeText: jest.fn().mockResolvedValue(undefined),
  },
});

// Mock timers
jest.useFakeTimers();

describe("useClipboard", () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it("should copy text to clipboard", async () => {
    const { result } = renderHook(() => useClipboard());

    await act(async () => {
      const success = await result.current.copyToClipboard("test text");
      expect(success).toBe(true);
    });

    expect(navigator.clipboard.writeText).toHaveBeenCalledWith("test text");
    expect(result.current.isCopied).toBe(true);
  });

  it("should reset copied state after timeout", async () => {
    const { result } = renderHook(() => useClipboard(1000));

    await act(async () => {
      await result.current.copyToClipboard("test");
    });

    expect(result.current.isCopied).toBe(true);

    act(() => {
      jest.advanceTimersByTime(1000);
    });

    expect(result.current.isCopied).toBe(false);
  });

  it("should handle clipboard errors", async () => {
    const writeTextMock = jest
      .fn()
      .mockRejectedValue(new Error("Permission denied"));
    Object.assign(navigator, {
      clipboard: { writeText: writeTextMock },
    });

    const { result } = renderHook(() => useClipboard());

    await act(async () => {
      const success = await result.current.copyToClipboard("test");
      expect(success).toBe(false);
    });

    expect(result.current.isCopied).toBe(false);
  });

  it("should clear copied state manually", async () => {
    const { result } = renderHook(() => useClipboard());

    await act(async () => {
      await result.current.copyToClipboard("test");
    });

    expect(result.current.isCopied).toBe(true);

    act(() => {
      result.current.clearCopied();
    });

    expect(result.current.isCopied).toBe(false);
  });
});

FAQ

Changelog

  • 2.0.0 — Enhanced TypeScript support, improved fallback handling, better error management, comprehensive documentation
  • 1.2.0 — Added clearCopied function and customizable reset time
  • 1.1.0 — Improved browser compatibility with execCommand fallback
  • 1.0.0 — Initial release with basic clipboard functionality