Skip to contentVibraUI
Foundation

useCopyToClipboard

Copies text to the clipboard and reports a timed confirmation flag for UI feedback.

Falls back to a hidden textarea + document.execCommand("copy") when the Clipboard API is unavailable.

Install

npx shadcn@latest add @vibra/use-copy-to-clipboard

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
timeoutnumber1500Milliseconds before copied resets to false.
returns.copiedboolean—True for timeout ms after a successful copy.
returns.copy(text: string) => Promise<boolean>—Copies text; resolves true on success, false if every copy strategy fails.

Dependencies

Registry

Source

hooks/use-copy-to-clipboard.ts
import * as React from "react"

/** Writes `text` to the clipboard: the async Clipboard API first, falling back to a hidden textarea + execCommand. */
async function writeToClipboard(text: string): Promise<boolean> {
  if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text)
      return true
    } catch {
      // Denied or unavailable (insecure origin) — fall through to the legacy path.
    }
  }
  if (typeof document === "undefined") return false
  let textarea: HTMLTextAreaElement | undefined
  try {
    textarea = document.createElement("textarea")
    textarea.value = text
    textarea.style.position = "fixed"
    textarea.style.opacity = "0"
    document.body.appendChild(textarea)
    textarea.focus()
    textarea.select()
    return document.execCommand("copy")
  } catch {
    return false
  } finally {
    // Optional-chained: `textarea` is still undefined if `createElement`
    // itself is what threw. Otherwise `remove()` is a no-op if `appendChild`
    // above never ran, so this is safe to call unconditionally — the
    // textarea must never linger in the DOM, whatever failed or succeeded.
    textarea?.remove()
  }
}

/** Copies text to the clipboard and reports a timed `copied` flag for UI feedback (e.g. swapping a copy icon for a checkmark). */
export function useCopyToClipboard(timeout = 1500): {
  copied: boolean
  copy: (text: string) => Promise<boolean>
} {
  const [copied, setCopied] = React.useState(false)
  const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => {
    return () => {
      if (timeoutRef.current) clearTimeout(timeoutRef.current)
    }
  }, [])

  const copy = React.useCallback(
    async (text: string) => {
      const ok = await writeToClipboard(text)
      setCopied(ok)
      if (timeoutRef.current) clearTimeout(timeoutRef.current)
      if (ok) timeoutRef.current = setTimeout(() => setCopied(false), timeout)
      return ok
    },
    [timeout]
  )

  return { copied, copy }
}