# Copy Button

> Copy to clipboard button for React and shadcn/ui with a legacy fallback and an animated check icon, for code and API keys.

Source: https://www.cult-ui.com/docs/components/copy-button

## Example

```tsx title="copy-button-demo.tsx"
"use client"

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

const sampleCode = "npm install @radix-ui/react-slot"
const sampleUrl = "https://github.com/vercel/next.js"

function CopyableRow({
  value,
  children,
}: {
  value: string
  children: React.ReactNode
}) {
  return (
    <div
      className={[
        "group relative flex w-full max-w-xl items-stretch overflow-hidden rounded-2xl border border-border/70",
        "bg-gradient-to-b from-muted/40 to-muted/20 text-left",
        "shadow-[0_1px_2px_rgba(0,0,0,0.05),0_1px_0_0_rgba(255,255,255,0.06)_inset]",
        "ring-1 ring-black/[0.04] dark:from-muted/25 dark:to-muted/10 dark:ring-white/[0.06]",
        "transition-[box-shadow,background-color,border-color] duration-200 ease-out",
        "[@media(hover:hover)]:hover:border-border [@media(hover:hover)]:hover:shadow-[0_4px_14px_rgba(0,0,0,0.08)]",
        "dark:[@media(hover:hover)]:hover:shadow-[0_4px_18px_rgba(0,0,0,0.35)]",
      ].join(" ")}
    >
      <div className="min-w-0 flex-1 px-4 py-3.5 leading-snug">{children}</div>
      <div
        className={[
          "flex shrink-0 items-center border-border/60 border-l bg-muted/30 px-1 py-1.5",
          "dark:bg-muted/15",
          "[@media(hover:hover)]:group-hover:bg-muted/45 dark:[@media(hover:hover)]:group-hover:bg-muted/25",
        ].join(" ")}
      >
        <CopyButton
          className="text-muted-foreground [@media(hover:hover)]:hover:text-foreground"
          layout="inline"
          size="icon-sm"
          value={value}
          variant="ghost"
        />
      </div>
    </div>
  )
}

function CopyButtonDemo() {
  return (
    <div className="flex flex-col items-center gap-5 p-6">
      <CopyableRow value={sampleCode}>
        <code className="font-mono text-foreground text-sm tracking-tight antialiased">
          {sampleCode}
        </code>
      </CopyableRow>
      <CopyableRow value={sampleUrl}>
        <span className="text-pretty text-muted-foreground text-sm leading-relaxed">
          {sampleUrl}
        </span>
      </CopyableRow>
    </div>
  )
}

export default CopyButtonDemo
```

Copy Button is a React button component for shadcn/ui, animated with Motion. Use it beside code snippets, API keys, or share links.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/copy-button
```

### Manual

**Install the required dependencies:**

```bash
npm install @tabler/icons-react
```

**Install the button component:**

```bash
npx shadcn@latest add button
```

**Copy and paste the following code into your project.**

```tsx title="copy-button.tsx"
"use client"

import * as React from "react"
import { Check, Copy } from "lucide-react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

const ICON_SPRING = {
  type: "spring" as const,
  duration: 0.3,
  bounce: 0,
}

function iconMotion(reduceMotion: boolean) {
  if (reduceMotion) {
    return {
      initial: { opacity: 1, scale: 1, filter: "blur(0px)" },
      animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
      exit: { opacity: 1, scale: 1, filter: "blur(0px)" },
      transition: { duration: 0 },
    }
  }
  return {
    initial: { opacity: 0, scale: 0.25, filter: "blur(4px)" },
    animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
    exit: { opacity: 0, scale: 0.25, filter: "blur(4px)" },
    transition: ICON_SPRING,
  }
}

function legacyCopyToClipboard(value: string) {
  const textArea = document.createElement("textarea")
  textArea.value = value
  textArea.setAttribute("readonly", "")
  textArea.style.position = "fixed"
  textArea.style.opacity = "0"
  textArea.style.pointerEvents = "none"

  document.body.appendChild(textArea)
  textArea.focus()
  textArea.select()
  textArea.setSelectionRange(0, value.length)

  let hasCopied = false
  try {
    hasCopied = document.execCommand("copy")
  } catch {
    hasCopied = false
  }

  document.body.removeChild(textArea)
  return hasCopied
}

export async function copyToClipboardWithMeta(value: string) {
  if (typeof window === "undefined") {
    return false
  }

  if (!value) {
    return false
  }

  let hasCopied = false

  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(value)
      hasCopied = true
    } catch {
      hasCopied = legacyCopyToClipboard(value)
    }
  } else {
    hasCopied = legacyCopyToClipboard(value)
  }

  if (!hasCopied) {
    return false
  }

  return true
}

export function CopyButton({
  value,
  className,
  variant = "ghost",
  layout = "inline",

  ...props
}: React.ComponentProps<typeof Button> & {
  value: string
  src?: string
  /** `overlay` = absolute corner for code blocks; `inline` = normal flow next to text */
  layout?: "inline" | "overlay"
  tooltip?: string
}) {
  const [hasCopied, setHasCopied] = React.useState(false)
  const reduceMotion = useReducedMotion()

  React.useEffect(() => {
    if (hasCopied) {
      const timer = setTimeout(() => setHasCopied(false), 2000)
      return () => clearTimeout(timer)
    }
  }, [hasCopied])

  const motionProps = iconMotion(!!reduceMotion)

  return (
    <Button
      aria-label={hasCopied ? "Copied to clipboard" : "Copy to clipboard"}
      className={cn(
        "before:-inset-2 touch-manipulation before:absolute before:z-[-1] before:content-['']",
        "motion-safe:active:scale-[0.96]",
        layout === "overlay" &&
          "absolute top-3 right-2 z-10 size-7 shrink-0 overflow-hidden bg-code focus-visible:opacity-100 [@media(hover:hover)]:hover:opacity-100",
        layout === "inline" && "relative shrink-0 overflow-hidden rounded-xl",
        className
      )}
      data-copied={hasCopied}
      data-slot="copy-button"
      onClick={async () => {
        const ok = await copyToClipboardWithMeta(value)

        if (ok) {
          setHasCopied(true)
        }
      }}
      size="icon"
      variant={variant}
      {...props}
    >
      <span aria-atomic="true" aria-live="polite" className="sr-only">
        {hasCopied ? "Copied" : ""}
      </span>
      <span
        aria-hidden="true"
        className="relative inline-flex size-4 items-center justify-center"
      >
        <AnimatePresence initial={false} mode="sync">
          {hasCopied ? (
            <motion.span
              className="absolute inset-0 flex items-center justify-center"
              key="check"
              {...motionProps}
            >
              <Check className="size-4" strokeWidth={2} />
            </motion.span>
          ) : (
            <motion.span
              className="absolute inset-0 flex items-center justify-center"
              key="copy"
              {...motionProps}
            >
              <Copy className="size-4" strokeWidth={2} />
            </motion.span>
          )}
        </AnimatePresence>
      </span>
    </Button>
  )
}
```

**Update the import paths to match your project setup.**
