shadimage
Delta Components
libclipboardsource: ok · refreshed

Clipboard

Copy text to the clipboard, falling back to a selection and execCommand where the async Clipboard API is unavailable — which includes any page served over plain HTTP, such as opening a dev server from a phone on the LAN. Returns whether the copy landed.

npx shadcn add "https://deltacomponents.dev/r/clipboard.json"
free

Where it goes

kanban

Interaction

dropdown

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Files1 · source cached 10/8/2026, 8:49:49 AM

registry/lib/clipboard.ts
/** Copy `value`, falling back when the async Clipboard API is unavailable.
 *
 *  `navigator.clipboard` only exists in a secure context — HTTPS, or localhost.
 *  Opening the docs from a phone on the LAN (http://192.168.x.x) is plain
 *  HTTP, so the object is simply absent and reading `.writeText` off it
 *  throws. The same applies to any consumer serving this component over HTTP
 *  on an internal network.
 *
 *  The fallback selects the text in an off-screen node and runs the legacy
 *  `execCommand("copy")`. It is deprecated but universally implemented, and it
 *  is the only path available without TLS. Returns whether the copy landed, so
 *  the button only claims success when it actually copied.
 *
 *  The node is a span, not a focused textarea. `execCommand("copy")` takes
 *  whatever is selected, and a selection needs no focus — while focusing a
 *  field (the usual recipe, which has to flip `contentEditable` on to make iOS
 *  select it at all) reads to iOS as "about to type": Safari collapses its
 *  bottom address bar for a keyboard that never arrives, then puts it back a
 *  frame later when the node is removed. Selecting a plain node skips the
 *  whole performance. `white-space: pre` keeps the newlines, which the copy
 *  takes from the rendered text rather than the string. */
export async function copyToClipboard(value: string): Promise<boolean> {
  try {
    if (navigator.clipboard?.writeText) {
      await navigator.clipboard.writeText(value)
      return true
    }
  } catch {
    // Present but refused (permissions policy, denied prompt) — try the node.
  }

  // Off-screen rather than invisible: text under `display:none` or
  // `visibility:hidden` is not selectable, so the copy would silently do
  // nothing. Parked at the viewport's top-left rather than off at -9999px —
  // selecting something makes the browser reveal it, and revealing a node
  // 9999px to the left is what made mobile lurch sideways on every copy.
  const holder = document.createElement("span")
  holder.textContent = value
  holder.style.cssText =
    "position:fixed;top:0;left:0;width:1px;height:1px;overflow:hidden;opacity:0;white-space:pre;user-select:text;-webkit-user-select:text"
  document.body.appendChild(holder)

  // Put whatever the reader had highlighted back afterwards.
  const selection = document.getSelection()
  const previous =
    selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null

  try {
    const range = document.createRange()
    range.selectNodeContents(holder)
    selection?.removeAllRanges()
    selection?.addRange(range)
    return document.execCommand("copy")
  } catch {
    return false
  } finally {
    selection?.removeAllRanges()
    holder.remove()
    if (previous) selection?.addRange(previous)
  }
}

More from Delta Components

Browse all components in this library →