Original

Copy Button

Copies a value to the clipboard and confirms with a tick.

npx shadcn@latest add @doffy/ascii

Installation

npx shadcn@latest add @doffy/copy-button

Or, without the registry in components.json:

npx shadcn@latest add https://ui.doffy.com/r/copy-button.json

Usage

import { CopyButton } from "@/components/ui/copy-button"

const command = "npx shadcn@latest add @doffy/ascii"

export default function CopyButtonDemo() {
  return (
    <div className="flex items-center gap-3 border bg-background py-1 pr-1 pl-3 text-xs">
      <code>{command}</code>
      <CopyButton value={command} />
    </div>
  )
}

Source

Show copy-button.tsx
"use client"

import * as React from "react"
import { CheckIcon, CopyIcon } from "@phosphor-icons/react"
import { cn } from "cn"

import { Button } from "@/registry/doffy/ui/button"

type CopyButtonProps = React.ComponentProps<typeof Button> & {
  /** Text written to the clipboard. */
  value: string
}

function CopyButton({
  value,
  variant = "ghost",
  size = "icon-sm",
  className,
  onClick,
  ...props
}: CopyButtonProps) {
  const [copied, setCopied] = React.useState(false)

  React.useEffect(() => {
    if (!copied) return
    const timeout = setTimeout(() => setCopied(false), 1500)
    return () => clearTimeout(timeout)
  }, [copied])

  return (
    <Button
      data-slot="copy-button"
      data-copied={copied}
      variant={variant}
      size={size}
      aria-label={copied ? "Copied" : "Copy to clipboard"}
      className={cn(className)}
      onClick={async (event) => {
        onClick?.(event)
        try {
          await navigator.clipboard.writeText(value)
          setCopied(true)
        } catch {
          // Clipboard access can be denied (insecure context, permissions); stay silent.
        }
      }}
      {...props}
    >
      {copied ? <CheckIcon /> : <CopyIcon />}
    </Button>
  )
}

export { CopyButton }
export type { CopyButtonProps }