ComponentsCopy Button

Copy Button

New

Copies text in one click, then turns green and retypes its label to Copied.

Install it in one line

Available in v1.6.0

$npx devignerui add copy-button

Writes components/ui/copy-button.tsx and lists what it needs.

First time here

$npx devignerui init

Or by hand

  1. Needs React 18 or 19 and Tailwind CSS v4.
  2. Install the dependencies.

    $npm i cn motion @devigner-ui/icons devignerui
  3. Paste the source from into components/ui/copy-button.tsx.

Usage

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

export function Example() {
  return (
    <CopyButton
      value="pnpm add devignerui"
    />
  );
}

Custom composition

<CopyButton value={() => editor.getText()} label="Copy code" copiedLabel="Copied!" />

Copy an install command

Pass the text and log the result.

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

export function InstallCommand() {
  return (
    <CopyButton
      value="pnpm add devignerui"
      onCopy={(text) => console.log("copied", text)}
      onError={(error) => console.error(error)}
    />
  );
}

Copy a link created on the server

Return a promise from value and the button waits on it with a spinner.

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

export function ShareLink({ id }: { id: string }) {
  return (
    <CopyButton
      label="Copy link"
      copiedLabel="Link copied"
      value={() => fetch(`/api/share/${id}`).then((r) => r.text())}
    />
  );
}

Icon-only button in a code block

Small and square, with the label kept for screen readers.

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

export function CodeCopy({ code }: { code: string }) {
  return <CopyButton value={code} iconOnly size="sm" label="Copy code" />;
}

Every prop

value-
string | function

Text to copy. A function is read at click time and may return a promise.

copynavigator.clipboard.writeText
function

Writes the text. Swap it for rich content or a fallback.

status-
"idle" | "pending" | "copied" | "error"

Controlled state.

defaultStatus"idle"
"idle" | "pending" | "copied" | "error"

Initial state when uncontrolled.

onStatusChange-
function

Fires whenever the state changes.

onCopy-
function

Fires after the text is on the clipboard.

onError-
function

Fires when reading or writing fails.

labelCopy
string

Label at rest.

copiedLabelCopied
string

Label after a copy.

pendingLabelCopying
string

Label while an async value resolves.

errorLabelFailed
string

Label after a failed copy.

resetAfter1000
number

ms the copied or error state holds. 0 keeps it.

size"md"
"sm" | "md" | "lg"

Height: 36, 44 or 52px.

iconOnlyfalse
boolean

Hides the text; the label stays as the accessible name.

disabledfalse
boolean

Disables interaction.

How to use it

The live demo is a black Copy pill. Click it and the text goes to your clipboard, the pill turns green, the icon blurs into a check and the label retypes itself to Copied, then it settles back after a second.

When to use it

  • Copying a command, token, link or code snippet where the user needs to know the copy landed.
  • Any spot that already has a primary action style and needs a single compact copy control.

When to reach for something else

  • Icon-only copy affordances inside dense code blocks. A plain icon button takes less room.
  • Copying data that needs a format choice (CSV or JSON). Use a menu for that.

Keyboard

Enter / Space
Copies the value. Ignored while an async value is still resolving.

Accessibility

  • A real button element with visible focus.
  • Its accessible name follows the state (label, pendingLabel, copiedLabel, errorLabel), and a polite live region announces every state but idle.
  • aria-busy is set while an async value resolves, and data-status mirrors the state for styling.
  • With iconOnly the text is hidden but label stays the accessible name.
  • Honors prefers-reduced-motion and an ancestor <MotionConfig reducedMotion="always">, so an in-app motion switch works too.

Theming

  • Rests on bg-foreground with text-background, so it inverts with the theme. Copied uses bg-green-700 and error bg-red-600, both with white text.
  • data-status (idle, pending, copied, error) is on the button, so className can target a state, for example data-[status=copied]:bg-emerald-600.
  • size sets the height, padding, text and icon size: sm 36px, md 44px, lg 52px.
  • label, copiedLabel, pendingLabel and errorLabel set both the visible text and the accessible name, for translation.

Edge cases

  • The default copy uses navigator.clipboard, which needs a secure context (https or localhost). Any failure, in value or in copy, shows the error state and calls onError.
  • If value returns a promise, the button shows a spinner until it resolves. Clicks during that time are ignored.
  • Letters the two labels share stay put; only the differing ones swap, so similar labels animate best.
  • Clicking again while Copied copies again and restarts the resetAfter hold. resetAfter 0 keeps the copied or error state until status changes.
  • Pass status to drive the look yourself, for example to show Copied after a keyboard shortcut copies.

Troubleshooting

Nothing is copied and I get an error.

navigator.clipboard only works on https or localhost and after a user gesture. Serve the page securely and pass onError to show a message.

Stuck on something else? Ask in the Discord.

Some of these designs take cues from interfaces we've admired around the web. If one started as your idea and you'd like credit, message us on X @devignerui or in the Discord and we'll add it.