typescript css 128 lines · 3 tabs

React useCopyToClipboard Hook With execCommand Fallback and Reset Timeout

Shared by codesnips Sep 2026
3 tabs
import { useCallback, useState } from 'react';

type CopiedValue = string | null;
type CopyFn = (text: string) => Promise<boolean>;

function legacyCopy(text: string): boolean {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.setAttribute('readonly', '');
  textarea.style.position = 'fixed';
  textarea.style.top = '0';
  textarea.style.left = '0';
  textarea.style.opacity = '0';
  document.body.appendChild(textarea);

  try {
    textarea.select();
    textarea.setSelectionRange(0, text.length);
    return document.execCommand('copy');
  } catch (error) {
    return false;
  } finally {
    document.body.removeChild(textarea);
  }
}

export function useCopyToClipboard(): [CopiedValue, CopyFn] {
  const [copiedText, setCopiedText] = useState<CopiedValue>(null);

  const copy: CopyFn = useCallback(async (text) => {
    try {
      if (navigator?.clipboard?.writeText) {
        await navigator.clipboard.writeText(text);
      } else if (!legacyCopy(text)) {
        throw new Error('execCommand copy failed');
      }

      setCopiedText(text);
      return true;
    } catch (error) {
      console.warn('Copy to clipboard failed', error);
      setCopiedText(null);
      return false;
    }
  }, []);

  return [copiedText, copy];
}
3 files · typescript, css Explain with highlit

This snippet shows how to build a reliable copy-to-clipboard feature in React that works across modern and legacy browsers. The core problem is that the modern navigator.clipboard.writeText API is asynchronous, requires a secure context (HTTPS or localhost), and may be blocked or unavailable, so a robust implementation needs a fallback path and clear status feedback for the UI.

In useCopyToClipboard hook, the hook returns a tuple of [copiedText, copy] similar to useState, which keeps the API familiar. The copy function is wrapped in useCallback so it stays referentially stable across renders and is safe to pass to child components or effect dependencies. It first checks for navigator?.clipboard; when present it awaits writeText, and when absent it delegates to legacyCopy, a small helper that recreates the classic document.execCommand('copy') trick using an off-screen <textarea>. That helper positions the element with fixed and opacity: 0 so it never causes a visual flash or scroll jump, selects its contents, and cleans up the node in a finally block.

On success copy stores the copied string in state and returns true; on failure it logs a warning, resets state to null, and returns false. Returning a boolean lets callers react to failures without subscribing to state, which is useful for toasts or analytics.

In CopyButton component, the hook is composed with a short-lived copied flag driven by useState and a useEffect timer. When a copy succeeds the button flips to a confirmed label for two seconds, then a cleanup function clears the setTimeout to avoid updating an unmounted component. This separation matters: the hook owns the what (the last copied value), while the component owns the when (the transient UI state), keeping the hook reusable in contexts that need no visual reset.

Accessibility is handled with aria-live on a visually adjacent status region and a dynamic aria-label, so screen readers announce the result. The button is disabled while there is no value, guarding against copying empty strings. Together these files demonstrate a production pattern: an isolated, testable hook plus a thin presentational component that adds feedback and a11y, with graceful degradation for older or restricted environments.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab

Share this code

Here's the card — post it anywhere.

React useCopyToClipboard Hook With execCommand Fallback and Reset Timeout — share card
Link copied