# Copy Button

A button that copies text to the clipboard with optional feedback.

## Installation

```bash
npx shadcn@latest add https://ui.jarv.is/r/copy-button.json
```

[Registry JSON](https://ui.jarv.is/r/copy-button.json)

## Preview

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

export function Preview() {
  return (
    <div className="flex w-full max-w-sm flex-wrap items-center justify-center gap-2">
      <CopyButton value="bunx --bun shadcn@latest add https://ui.jarv.is/r/copy-button.json" />
      <CopyButton
        value="jui_live_7vK8x8e8f95hK2hVv4sYQxB3"
        variant="outline"
        showLabel
        copyLabel="Copy token"
      />
    </div>
  );
}
```


## Source

### ui/copy-button.tsx

```tsx
"use client";

import { IconCheck, IconCopy } from "@tabler/icons-react";
import * as React from "react";

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

type CopyButtonClickEvent = Parameters<
  NonNullable<React.ComponentProps<typeof Button>["onClick"]>
>[0];

type CopyButtonProps = Omit<React.ComponentProps<typeof Button>, "children" | "value"> & {
  value: string | (() => string);
  showLabel?: boolean;
  copyLabel?: string;
  copiedLabel?: string;
  resetDelay?: number;
  onCopied?: (value: string, event: CopyButtonClickEvent) => void;
  onCopyError?: (error: unknown, event: CopyButtonClickEvent) => void;
};

function CopyButton({
  value,
  showLabel = false,
  copyLabel = "Copy to clipboard",
  copiedLabel = "Copied",
  resetDelay = 2000,
  variant = "ghost",
  size,
  className,
  disabled,
  onClick,
  onCopied,
  onCopyError,
  ...props
}: CopyButtonProps) {
  const resetTimeoutRef = React.useRef<number | undefined>(undefined);
  const [copied, setCopied] = React.useState(false);
  const buttonSize: React.ComponentProps<typeof Button>["size"] =
    size ?? (showLabel ? "sm" : "icon-sm");

  React.useEffect(() => {
    return () => window.clearTimeout(resetTimeoutRef.current);
  }, []);

  const handleClick = React.useCallback(
    async (event: CopyButtonClickEvent) => {
      onClick?.(event);

      if (event.defaultPrevented || copied || disabled) {
        return;
      }

      try {
        const textToCopy = typeof value === "function" ? value() : value;

        await copyTextToClipboard(textToCopy);
        window.clearTimeout(resetTimeoutRef.current);
        setCopied(true);
        resetTimeoutRef.current = window.setTimeout(() => setCopied(false), resetDelay);
        onCopied?.(textToCopy, event);
      } catch (error) {
        setCopied(false);
        onCopyError?.(error, event);
      }
    },
    [copied, disabled, onClick, onCopied, onCopyError, resetDelay, value],
  );

  return (
    <Button
      type="button"
      variant={variant}
      size={buttonSize}
      disabled={disabled}
      aria-label={copied ? copiedLabel : copyLabel}
      className={cn("shrink-0", copied && "cursor-default", className)}
      onClick={(event) => {
        void handleClick(event);
      }}
      {...props}
    >
      {copied ? (
        <IconCheck aria-hidden="true" data-icon={showLabel ? "inline-start" : undefined} />
      ) : (
        <IconCopy aria-hidden="true" data-icon={showLabel ? "inline-start" : undefined} />
      )}
      {showLabel && <span>{copied ? copiedLabel : copyLabel}</span>}
    </Button>
  );
}

async function copyTextToClipboard(value: string) {
  if (navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(value);
    return;
  }

  const textArea = document.createElement("textarea");

  textArea.value = value;
  textArea.setAttribute("readonly", "");
  textArea.style.position = "fixed";
  textArea.style.inset = "0 auto auto -9999px";
  document.body.append(textArea);
  textArea.select();

  const copied = document.execCommand("copy");

  textArea.remove();

  if (!copied) {
    throw new Error("Copy command was rejected.");
  }
}

export { CopyButton, type CopyButtonProps };
```



## Usage

### Icon button

Use the copy button for command snippets, tokens, URLs, and other short values.

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

export function Example() {
  return <CopyButton value="npm install" showLabel copyLabel="Copy command" />;
}
```

### Labeled button

Show a label when the copy action is one of several visible actions in a toolbar or card.

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

export function CopyInstallCommand() {
  return (
    <CopyButton
      value="bunx --bun shadcn@latest add https://ui.jarv.is/r/copy-button.json"
      showLabel
      copyLabel="Copy command"
    />
  );
}
```

### Dynamic values

Pass a function when the value should be read at click time, such as a generated token.

```tsx
import { useRef } from "react";

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

export function CopyLatestToken() {
  const tokenInputRef = useRef<HTMLInputElement>(null);

  return (
    <div className="flex items-center gap-2">
      <input ref={tokenInputRef} defaultValue="jui_live_..." readOnly />
      <CopyButton value={() => tokenInputRef.current?.value ?? ""} copyLabel="Copy token" />
    </div>
  );
}
```

### Copy callbacks

Use `onCopied` and `onCopyError` to connect copy events to your app feedback.

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

export function CopyInviteLink({ inviteUrl }: { inviteUrl: string }) {
  return (
    <CopyButton
      value={inviteUrl}
      showLabel
      copyLabel="Copy invite"
      copiedLabel="Copied"
      onCopied={(value) => {
        console.log(`Copied ${value}`);
      }}
      onCopyError={(error) => {
        console.error("Could not copy invite link", error);
      }}
    />
  );
}
```

