# Copy Button

> Icon button that copies a value to the clipboard and morphs its glyph into a check with a scale-and-blur swap.

- Collection: Components
- Tier: free
- Docs: https://geturui.com/docs/copy-button
- Install: `npx shadcn@latest add @geturui/copy-button`
- Dependencies: motion, lucide-react

## Usage

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

export function InstallCommand() {
  const command = "npx shadcn@latest add https://geturui.dev/r/copy-button.json";
  return (
    <div className="flex items-center gap-3 rounded-lg bg-surface-sunken py-2 pl-4 pr-2">
      <code className="flex-1 truncate font-mono text-sm">{command}</code>
      <CopyButton value={command} label="Copy install command" />
    </div>
  );
}
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| value | `string` | — | Text written to the clipboard. |
| label | `string` | "Copy" | Accessible name before copying. |
| onCopy | `(value: string) => void` | — | Called after each click, even if the clipboard is blocked. |
| className | `string` | — | Classes for the button. |

## Accessibility

- A real <button> whose accessible name switches from the label to "Copied" for 1.6s.
- Still gives visual feedback when the clipboard API is blocked; use onCopy to announce status in your own live region.
- Visible focus ring and a 32px hit area.

## Source

### components/ui/copy-button.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { Check, Copy } from "lucide-react";
import { cn } from "@/lib/utils";

export interface CopyButtonProps {
  value: string;
  label?: string;
  onCopy?: (value: string) => void;
  className?: string;
}

/** Copies `value`; the glyph morphs into a check (scale + blur) for 1.6s. */
export function CopyButton({ value, label = "Copy", onCopy, className }: CopyButtonProps) {
  const [copied, setCopied] = React.useState(false);
  const t = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  React.useEffect(() => () => {
    if (t.current) clearTimeout(t.current);
  }, []);

  async function copy() {
    try {
      await navigator.clipboard.writeText(value);
    } catch {
      /* clipboard blocked — still show feedback */
    }
    setCopied(true);
    onCopy?.(value);
    if (t.current) clearTimeout(t.current);
    t.current = setTimeout(() => setCopied(false), 1600);
  }

  return (
    <motion.button
      type="button"
      whileTap={{ scale: 0.9 }}
      onClick={copy}
      aria-label={copied ? "Copied" : label}
      className={cn(
        "relative inline-grid size-8 shrink-0 place-items-center rounded-[10px] text-ink-muted outline-none transition-colors hover:bg-surface-hover hover:text-ink focus-visible:shadow-[var(--focus-ring)]",
        className,
      )}
    >
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={copied ? "check" : "copy"}
          initial={{ opacity: 0, scale: 0.4, filter: "blur(4px)" }}
          animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
          exit={{ opacity: 0, scale: 0.4, filter: "blur(4px)" }}
          transition={{ type: "spring", stiffness: 500, damping: 25 }}
          className="col-start-1 row-start-1"
        >
          {copied ? <Check className="size-4 text-success" strokeWidth={2.25} /> : <Copy className="size-4" />}
        </motion.span>
      </AnimatePresence>
    </motion.button>
  );
}

```
