---
title: Copy Button
description: A glass chip that writes to the clipboard and confirms in the button itself.
source: https://adamjurek.com/components/copy-button
---

## Home


Copy, with the confirmation where the click happened. The chip swaps to a tick
and "Copied" for two seconds, then hands back - no toast, no portal, nothing
that has to know where the corner of the screen is.

<!-- ::start:showcase demo="copy-button" height="320" -->
<!-- ::end:showcase -->

## Why it is built this way

A toast library for one word is the wrong trade, but a `setState` after
unmount is still a leak, so the reset timer is cleared on unmount. The chip is
glass - fill plus edge, no blur - and it comes in two grounds: `slab` for the
charcoal of a code block, `paper` for everywhere else, because one glass recipe
cannot sit on both materials.

On fine pointers the chip appears on hover of its `code-shell`; on coarse
pointers it is always visible, because "appears on hover" is a desktop fiction
a phone cannot perform.

## What it does not do

It does not fall back to `document.execCommand`. `navigator.clipboard` exists
everywhere this site runs; where a permission denies it, the button simply
never confirms, which is the truthful rendering of what happened.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/copy-button.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/copy-button.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Rendering |
| Files | `copy-button.tsx` |
| Dependencies | None |
| Also installs | `icon` |
| Tags | clipboard, button |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { CopyButton } from "@sushindustries/ui";

export function Example() {
	return (
		<code className="code">
			pnpm add @sushindustries/ui
			<CopyButton text="pnpm add @sushindustries/ui" ground="paper" />
		</code>
	);
}
```

## What you should see

A glass chip reading "Copy" with a copy glyph. Click it and the glyph swaps to
a tick, the label reads "Copied", and after two seconds it hands back to the
resting state on its own - no toast, no second element appearing anywhere
else on the page.

## If nothing happens

If clicking never shows "Copied", the page is not in a secure context (plain
HTTP rather than HTTPS or localhost) - `navigator.clipboard` does not exist
there, and the button treats that the same as a denied permission: it fails
silently rather than throwing, so check the browser console is quiet, not
loud, before assuming this is broken.


## Guides


## Choosing a ground

```tsx
<CopyButton text={code} ground="slab" />   {/* the charcoal of a code block */}
<CopyButton text={command} ground="paper" />  {/* everywhere else */}
<CopyButton text={value} ground="accent" />
```

The chip is glass either way - fill plus edge, no blur - but one glass recipe
does not read correctly on every background, so `ground` picks which recipe.
Get it wrong and the button is still clickable, just faint or oddly bright
against whatever it sits on.

## Composing it

`ground` describes a material, so give it something with a visible boundary
to sit on - a code block, an inline `<code>`, a card - rather than the bare
page background. On fine pointers, showing it only on hover of a `code-shell`
ancestor is common; on coarse pointers keep it always visible, since
"appears on hover" cannot happen without a hover to begin with.

## When not to use it

Copying something longer than a short string - a whole file, a large JSON
blob a reader would want to inspect before trusting - is better served by
`CodeBlock`, which pairs the same button with the content it is copying.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `text` | `string` | - | What lands on the clipboard. |
| `label?` | `string` | `"Copy"` | Visible label at rest. The copied state always says "Copied". |
| `ground?` | `"slab" \| "paper" \| "accent"` | `"slab"` | Which material the chip sits on. `slab` is the charcoal of a code block; `paper` is everywhere else. The chip is glass either way - the ground decides what the glass is made of. |
| `icon?` | `IconName` | `"copy"` | Leading glyph at rest. The tick still takes over while copied. |

<!-- /generated:api -->

## Notes

`ground="slab"` is the default and also the value that gets no `data-ground`
attribute at all - the stylesheet's un-attributed rule already draws the slab
recipe, so `slab` is "say nothing" rather than "say slab explicitly".

There is no `onCopy` callback and no way to read whether the last copy
succeeded from outside the component - the copied state is purely visual and
resets on its own timer. A caller that needs to know the result should call
`navigator.clipboard` itself rather than trying to observe this button.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="copy-button" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { CopyButton } from "@sushindustries/ui";

export function ApiKeyRow({ value }: { value: string }) {
	return (
		<div className="flex items-center justify-between gap-3">
			<code className="mono text-sm">{value.slice(0, 8)}…</code>
			<CopyButton text={value} label="Copy key" ground="paper" />
		</div>
	);
}
```

## What this example is not

Not a masked-value component. The truncated display is for reading; the full
`value` still goes to the clipboard on click, which is the point - do not
reuse this pattern somewhere the truncated text is the only thing that should
ever leave the row.
