# Copy Button

Copy a value to the clipboard with an icon button or a labeled button that confirms with a check, a tooltip and an announcement.

Client portal address with a copy action

[Complete TSX file](/examples/copy-button/default.tsx)

```tsx
"use client";

import {
  Card,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";
import { CopyButton } from "@/siteplane/components/copy-button";
import {
  DataList,
  DataListItem,
  DataListLabel,
  DataListValue,
} from "@/siteplane/components/data-list";

export default function Example() {
  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Website</CardTitle>
      </CardHeader>
      <CardPanel>
        <DataList>
          <DataListItem>
            <DataListLabel>Client portal</DataListLabel>
            <DataListValue className="flex items-center gap-1">
              <span className="min-w-0">portal.northlight.studio</span>
              <CopyButton
                labels={{ copy: "Copy address" }}
                value="https://portal.northlight.studio"
              />
            </DataListValue>
          </DataListItem>
          <DataListItem>
            <DataListLabel>Status</DataListLabel>
            <DataListValue>Live since March 4</DataListValue>
          </DataListItem>
        </DataList>
      </CardPanel>
    </Card>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/copy-button
```

With npm: `npx shadcn@4.18.0 add @siteplane/copy-button`. [Registry JSON](/registry/copy-button.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/card @siteplane/data-list @siteplane/input @siteplane/input-group
```

Field is installed with Input.

## Usage

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

<CopyButton value="https://portal.northlight.studio" />;
```

## Icon only

Without children, `CopyButton` shows the copy icon alone, as the muted inline action: the bare icon in a 24 px target, muted at rest and darker on hover, press and keyboard focus. Place it after the value it copies, such as an address, an ID or an invite link. Name it after what it copies with `labels.copy` or `aria-label`, such as “Copy address”.

A press writes `value` to the clipboard. The icon turns into a check for two seconds and then draws itself again, the tooltip changes from “Copy” to “Copied”, and screen readers hear “Copied”.

## With text

Children make a text button: the icon leads and the label follows. While the check shows, the label reads `labels.copied`, “Copied” by default. Set `variant` for the look of another Button, such as `outline`.

Copy link as a quiet text action and as an outline Button

[Complete TSX file](/examples/copy-button/text.tsx)

```tsx
"use client";

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

const invite = "https://app.northlight.studio/join/7f3k2";

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <CopyButton value={invite}>Copy link</CopyButton>
      <CopyButton
        labels={{ copied: "Invite link copied" }}
        value={invite}
        variant="outline"
      >
        Copy invite link
      </CopyButton>
    </div>
  );
}
```

## In an input group

Inside a field, set `profile="field-action"`, the profile of every icon action in a field. The icon sits at the end of the field, as the show and hide action of a password does.

Read-only invite link with a copy action

[Complete TSX file](/examples/copy-button/input-group.tsx)

```tsx
"use client";

import { CopyButton } from "@/siteplane/components/copy-button";
import { Field, FieldLabel } from "@/siteplane/components/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/siteplane/components/input-group";

const invite = "https://app.northlight.studio/join/7f3k2";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Invite link</FieldLabel>
      <InputGroup>
        <InputGroupInput readOnly value={invite} />
        <InputGroupAddon align="inline-end">
          <CopyButton
            labels={{ copied: "Invite link copied", copy: "Copy invite link" }}
            profile="field-action"
            value={invite}
          />
        </InputGroupAddon>
      </InputGroup>
    </Field>
  );
}
```

## Copied and failed

A copy that works shows the check. A copy that fails, because the browser has no clipboard, refuses it, or the value could not be loaded, says `labels.copyFailed`, “Copy unavailable” by default, for a moment: as a cross with that tooltip on the icon alone, in the label of a text button, and to screen readers. `onCopy` receives the copied text, and `onCopyError` receives the error, for example to show a toast with what to do next.

A copy that works and a copy whose value fails to load

[Complete TSX file](/examples/copy-button/states.tsx)

```tsx
"use client";

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

/** Loads the key when the button is pressed; here the request fails. */
function loadApiKey(): Promise<string> {
  return Promise.reject(new Error("The key could not be loaded."));
}

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-6 text-sm">
      <span className="flex items-center gap-1">
        Workspace ID
        <CopyButton labels={{ copy: "Copy workspace ID" }} value="ws_8f2k41" />
      </span>
      <span className="flex items-center gap-1">
        API key
        <CopyButton
          labels={{
            copy: "Copy API key",
            copyFailed: "The key could not be loaded",
          }}
          value={loadApiKey}
        />
      </span>
    </div>
  );
}
```

## A value that loads

`value` can be a function that returns the text, also asynchronously, such as a token requested when the button is pressed. Browsers allow a copy only shortly after the press, so keep that request short. In Safari the text goes in as a pending clipboard item, which keeps the press valid while it loads.

```tsx
<CopyButton labels={{ copy: "Copy API token" }} value={() => fetchToken()} />
```

## Looks

Button props pass through. A `variant` alone, such as `outline` or `ghost`, gives the CopyButton the look of that Button, at the icon square of its `size`. `profile`, `shape`, `className` and `render` work as on [Button](/docs/components/button/).

```tsx
<CopyButton size="icon-md" value={hex} variant="outline" />
```

`tooltip={false}` turns the tooltip off. An object passes props to `TooltipPopup`, such as `side`.

## Copy from a menu

A menu entry that copies stays a `MenuItem` with the `Copy` icon. Copy in its `onClick` with `useCopyToClipboard`, the copy of CopyButton without the button:

```tsx
const { copy } = useCopyToClipboard({ labels: { copied: "Link copied" } });

<MenuItem onClick={() => copy(link)}>
  <CopyIcon />
  Copy link
</MenuItem>;
```

The hook announces the outcome. `copied` and `failed` tell a control of your own which state to show.

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| CopyButton.value | string \| (() => string \| Promise<string>) | required | The text to copy, or a function that returns it when the button is pressed. |
| CopyButton.children | ReactNode | unset | A label after the icon, which makes a text button. Without one, the button shows the icon alone with a tooltip. |
| CopyButton.labels | Partial<CopyButtonLabels> | English | Replaces the words: copy (Copy, the name and tooltip of the icon alone), copied (Copied) and copyFailed (Copy unavailable). |
| CopyButton.onCopy | (value: string) => void | unset | Called with the text once it is on the clipboard. |
| CopyButton.onCopyError | (error: unknown) => void | unset | Called with the error when the clipboard is missing or refuses, or the value cannot be loaded. |
| CopyButton.tooltip | boolean \| CopyButtonTooltipProps | true | The tooltip of the icon alone. false turns it off; an object passes props to TooltipPopup. |
| CopyButton.variant / size / shape / profile | Button props | muted inline action | Without profile and variant, the icon alone is the muted inline action at icon-xs. A variant gives the look of that Button; profile field-action places it inside a field. |
| useCopyToClipboard | (options?) => { copy, copied, failed, phase } | announce: true | The copy of CopyButton for another control. Options: announce, labels (copied, copyFailed), onCopy and onCopyError. copy(value) resolves true once the text is on the clipboard. |
`CopyButtonProps` extends the Button props except `value`, `children` and the
native `onCopy`, and accepts a ref. `aria-label` names the icon alone at rest,
in place of `labels.copy`. The check shows for two seconds and a failure for a
little longer; both are said once in a polite live region, which stays readable
while a dialog is open. The tooltip stays open on a press, so it can say
“Copied”, and is hidden from assistive technology, which hears the button’s
name. The type `CopyButtonLabels` is exported.

[Read the installed source](/source/copy-button).

## Usage notes

Browsers let a page write to the clipboard only in a secure context, on `https` or `localhost`, and in response to a press. Elsewhere, such as on plain `http`, CopyButton copies through a text selection, which a browser may refuse; the failure state then shows.

Show the value next to the button, or in the field it belongs to, so people can still select it when a copy fails. For a block of code, set `copyable` on [CodeBlock](/docs/components/code/), which places this button in the block.
