Hooks

useCopyToClipboard

A clipboard-copy hook with an auto-resetting copied state and a non-throwing error channel.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

export interface UseCopyToClipboardOptions {
  /** How long `copied` stays true before falling back to false, in ms. Defaults to 2000. */
  resetDelay?: number
}

export interface UseCopyToClipboardResult {
  copied: boolean
  copy: (text: string) => Promise<boolean>
  error: Error | null
}

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/use-copy-to-clipboard.json

Prompt

Build a React + TypeScript "useCopyToClipboard" hook (no dependencies beyond
React; uses the browser Clipboard API only).

Contract
- `useCopyToClipboard(options?: { resetDelay?: number }): { copied: boolean;
  copy: (text: string) => Promise<boolean>; error: Error | null }`.
- `resetDelay` defaults to 2000ms.
- `copy(text)` returns a `Promise<boolean>` — `true` on a successful clipboard
  write, `false` on any failure. It never throws.

Behavior
- On `copy(text)`: guard for `navigator.clipboard.writeText` existing (SSR,
  insecure http origin, unsupported browser). If missing, set `error` and
  resolve `false` without touching `copied`.
- On a successful write: set `error` to `null`, set `copied` to `true`, and
  (re)start a timer that flips `copied` back to `false` after `resetDelay`.
  Calling `copy` again while already `copied` clears and restarts the timer
  instead of stacking multiple timers.
- On a rejected write (permission denied, missing user gesture, clipboard
  blocked): set `error` to the rejection reason (wrapped in an `Error` if it
  wasn't already one) and resolve `false`; `copied` is left untouched — a
  failed copy never flips it to true.
- The reset timer is cleared on unmount so no `setState` fires after the
  consuming component is gone.
- Every state update happens inside the clipboard promise's resolve/reject
  branch. The hook body itself never touches `navigator` synchronously during
  render or in a bare mount effect — the browser API only runs inside the
  event-triggered `copy` call — so the hook is safe to use from a
  server-rendered component.
- Multiple components can call `useCopyToClipboard()` independently in the
  same tree; each call owns its own `copied` / `error` / timer, there is no
  shared or global copied state.

Rendering & styling
- The hook renders nothing itself. Consumers own all UI: swap an icon/label
  when `copied` flips, use semantic tokens (`bg-primary`, `text-destructive`,
  `text-muted-foreground`) for any visual feedback, and respect
  `prefers-reduced-motion` for any icon-swap transition.

Customization levers
- `resetDelay` — how long the copied feedback stays before falling back to idle.
- Add `onSuccess(text)` / `onError(error)` callbacks if a consumer needs side
  effects (analytics, toast) beyond reading `copied` / `error` — intentionally
  left out by default to keep the contract minimal.
- A `document.execCommand("copy")` fallback for very old browsers is
  deliberately NOT included: it's deprecated, needs a hidden textarea +
  selection hack, and every target environment (evergreen browsers, https or
  localhost) already ships `navigator.clipboard`. Only add it if you must
  support such a browser.

Concepts

  • Timed state with restart-on-repeatcopied isn't a one-shot flag; it's a timer-backed state that a fresh copy() call always restarts, so rapid re-clicks keep extending the "copied" window instead of flickering back to idle mid-read.
  • Non-throwing error channel — failures (permission denial, insecure origin, unsupported browser) resolve to false and populate error, never throw; callers can if (!(await copy(text))) without a try/catch.
  • Multi-instance independence — the hook holds no module-level state, so a list of copy buttons each call useCopyToClipboard() and animate their own copied state without stepping on each other.
  • SSR-safe browser accessnavigator.clipboard is only read inside the copy callback (fired by a user event), never during render or in a mount-time effect, so the hook doesn't need a typeof window guard to be server-render safe.

On This Page