# Copy Button

Copy text to clipboard with visual feedback and animation.

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

export function CopyButtonDemo() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <CopyButton variant="secondary" text="Text 1" />

      <CopyButton
        className="pr-3 pl-2.5 will-change-transform"
        variant="outline"
        size="default"
        text="Text 2"
      >
        Copy
      </CopyButton>
    </div>
  )
}

```

## Features

* Smooth icon transitions with animated state changes using Motion.
* Visual feedback with three distinct states: idle, success, and error.
* Accepts a string or function for dynamic content.
* Callback support for success and error events.

## Installation

<CodeTabs>
  <TabsListInstallType />

  <TabsContent value="cli">
    ```bash
    npx shadcn add @ncdai/copy-button
    ```
  </TabsContent>

  <TabsContent value="manual">
    <Steps>
      <Step>Install the following dependencies</Step>

      ```bash
      npm install clsx tailwind-merge class-variance-authority lucide-react motion radix-ui
      ```

      <Step>Add a cn helper</Step>

      ```ts title="lib/utils.ts" showLineNumbers
      import type { ClassValue } from "clsx"
      import { clsx } from "clsx"
      import { twMerge } from "tailwind-merge"

      export const cn = (...inputs: ClassValue[]) => {
        return twMerge(clsx(inputs))
      }

      ```

      <Step>Install the required components</Step>

      * [Button](https://ui.shadcn.com/docs/components/button)

      <Step>Copy and paste the following code into your project</Step>

      ```ts title="hooks/use-copy-to-clipboard.ts" showLineNumbers
      "use client"

      import { useCallback, useRef, useState } from "react"

      export type CopyState = "idle" | "done" | "error"

      export type UseCopyToClipboardOptions = {
        onCopySuccess?: (text: string) => void
        onCopyError?: (error: Error) => void
        resetDelay?: number
      }

      export function useCopyToClipboard({
        onCopySuccess,
        onCopyError,
        resetDelay = 1500,
      }: UseCopyToClipboardOptions = {}) {
        const [state, setState] = useState<CopyState>("idle")
        const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

        const copy = useCallback(
          async (text: string | (() => string)) => {
            // Clear any pending reset
            if (resetTimeoutRef.current) {
              clearTimeout(resetTimeoutRef.current)
            }

            try {
              const finalText = typeof text === "function" ? text() : text
              await navigator.clipboard.writeText(finalText)
              setState("done")
              onCopySuccess?.(finalText)
            } catch (error) {
              setState("error")
              onCopyError?.(error instanceof Error ? error : new Error("Copy failed"))
            } finally {
              // Schedule reset to idle
              resetTimeoutRef.current = setTimeout(() => {
                setState("idle")
              }, resetDelay)
            }
          },
          [onCopyError, onCopySuccess, resetDelay]
        )

        return { state, copy } as const
      }

      ```

      ```tsx title="components/copy-button.tsx" showLineNumbers
      "use client"

      import { CheckIcon, CircleXIcon, CopyIcon } from "lucide-react"
      import type { HTMLMotionProps, Variants } from "motion/react"
      import { AnimatePresence, motion } from "motion/react"
      import type { ComponentProps } from "react"

      import { Button } from "@/components/ui/button"
      import type { CopyState } from "@/hooks/use-copy-to-clipboard"
      import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"

      export const motionIconVariants: Variants = {
        initial: { opacity: 0, scale: 0.8, filter: "blur(2px)" },
        animate: { opacity: 1, scale: 1, filter: "blur(0px)" },
        exit: { opacity: 0, scale: 0.8 },
      }

      export const motionIconProps: HTMLMotionProps<"span"> = {
        variants: motionIconVariants,
        initial: "initial",
        animate: "animate",
        exit: "exit",
        transition: { duration: 0.15, ease: "easeOut" },
      }

      export function CopyStateIcon({ state }: { state: CopyState }) {
        return (
          <AnimatePresence mode="popLayout" initial={false}>
            {state === "idle" ? (
              <motion.span key="idle" {...motionIconProps}>
                <CopyIcon />
              </motion.span>
            ) : state === "done" ? (
              <motion.span key="done" {...motionIconProps}>
                <CheckIcon strokeWidth={3} />
              </motion.span>
            ) : state === "error" ? (
              <motion.span key="error" {...motionIconProps}>
                <CircleXIcon />
              </motion.span>
            ) : null}
          </AnimatePresence>
        )
      }

      export type CopyButtonProps = ComponentProps<typeof Button> & {
        /** The text to copy, or a function that returns the text. */
        text: string | (() => string)
        /** Called with the copied text on successful copy. */
        onCopySuccess?: (text: string) => void
        /** Called with the error if the copy operation fails. */
        onCopyError?: (error: Error) => void
      }

      export function CopyButton({
        size = "icon",
        children,
        text,
        onCopySuccess,
        onCopyError,
        onClick,
        ...props
      }: CopyButtonProps) {
        const { state, copy } = useCopyToClipboard({
          onCopySuccess,
          onCopyError,
        })

        return (
          <Button
            size={size}
            onClick={(e) => {
              copy(text)
              onClick?.(e)
            }}
            aria-label="Copy"
            {...props}
          >
            <CopyStateIcon state={state} />
            {children}
          </Button>
        )
      }

      ```

      <Step>Update the import paths to match your project setup</Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage

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

```tsx
<CopyButton text="Some text to copy" />
```

## API Reference

### CopyButton

Extends [Button](https://ui.shadcn.com/docs/components/radix/button) with clipboard functionality.

<TypeTable
  id="type-table-props.ts-CopyButtonProps"
  type={{
  "id": "props.ts-CopyButtonProps",
  "name": "CopyButtonProps",
  "description": "",
  "entries": [
    {
      "name": "text",
      "description": "The text to copy, or a function that returns the text.",
      "tags": [],
      "type": "string | (() => string)",
      "simplifiedType": "function | string",
      "required": true,
      "deprecated": false
    },
    {
      "name": "onCopySuccess",
      "description": "Called with the copied text on successful copy.",
      "tags": [],
      "type": "((text: string) => void) | undefined",
      "simplifiedType": "function",
      "required": false,
      "deprecated": false
    },
    {
      "name": "onCopyError",
      "description": "Called with the error if the copy operation fails.",
      "tags": [],
      "type": "((error: Error) => void) | undefined",
      "simplifiedType": "function",
      "required": false,
      "deprecated": false
    }
  ]
}}
/>

See [shadcn/ui](https://ui.shadcn.com/docs/components/radix/button#api-reference) documentation for more information.


Last updated on March 1, 2026