typescript css 130 lines · 3 tabs

Frontend: copy-to-clipboard with fallback

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

export type CopyStatus = 'idle' | 'copied' | 'error';

function fallbackCopy(text: string): boolean {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.readOnly = true;
  textarea.style.position = 'fixed';
  textarea.style.top = '-9999px';
  textarea.style.opacity = '0';
  document.body.appendChild(textarea);
  try {
    textarea.focus();
    textarea.select();
    return document.execCommand('copy');
  } catch {
    return false;
  } finally {
    document.body.removeChild(textarea);
  }
}

export function useCopyToClipboard(resetDelay = 2000): [CopyStatus, (text: string) => Promise<void>] {
  const [status, setStatus] = useState<CopyStatus>('idle');
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const scheduleReset = useCallback(() => {
    if (timerRef.current) clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() => setStatus('idle'), resetDelay);
  }, [resetDelay]);

  const copy = useCallback(async (text: string) => {
    let ok = false;
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(text);
        ok = true;
      } else {
        ok = fallbackCopy(text);
      }
    } catch {
      ok = false;
    }
    setStatus(ok ? 'copied' : 'error');
    scheduleReset();
  }, [scheduleReset]);

  useEffect(() => {
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, []);

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

Copying text to the clipboard in the browser looks trivial until it has to work everywhere. The modern navigator.clipboard.writeText API is asynchronous, promise-based, and only available in secure contexts (HTTPS or localhost); older browsers and insecure origins still need the legacy document.execCommand('copy') path. This snippet packages both behind a single hook so components never have to branch on browser capability themselves.

The useCopyToClipboard hook returns a tuple of the current status and a copy callback, mirroring the shape of useState. The status is a small state machine — idle, copied, or error — which is enough to drive UI feedback without leaking implementation details. Inside copy, the code first checks navigator.clipboard and whether the page isSecureContext; when available it awaits writeText. Otherwise it falls back to fallbackCopy, which creates an off-screen <textarea>, selects its contents, and triggers the deprecated but widely-supported execCommand.

A few edge cases are handled deliberately. The fallback element uses position: fixed with zero opacity and readOnly so it never scrolls the page or flashes visibly, and it is always removed in a finally block even if selection throws. A setTimeout reference stored in a ref resets status back to idle after a delay, and the effect cleanup clears any pending timer so an unmounted component never calls setStatus — a common source of React warnings. The copy callback is wrapped in useCallback with a stable dependency so it can be passed to memoized children safely.

The CopyButton component shows the intended consumption pattern: it reads status to swap the label and aria-live announcement, disables itself briefly after success, and never needs to know which clipboard path executed. This separation is the real value — capability detection and timing live in one tested place, while components stay declarative. The trade-off is that the fallback requires a real user gesture to succeed (browsers block programmatic copies otherwise), so the hook should always be invoked from an event handler, never on mount or in an effect.


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
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 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
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs

Share this code

Here's the card — post it anywhere.

Frontend: copy-to-clipboard with fallback — share card
Link copied