Skip to content

CopyButton

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/copy-button.json
See it liveA button that appears on hover of the panel around it, and one whose clipboard refuses - which says so rather than drawing a tick.→

Whatever a product shows in a panel — code, a payload, a log, one side of a comparison — somebody eventually wants to take it away. This is that button, with the three things a hand-written one misses.

It confirms only after the clipboard does. navigator.clipboard.writeText can be refused: it needs a secure context and, in some browsers, a permission. A tick drawn on click is a lie in exactly the case the reader most needs the truth, and onCopy(false) is how your product finds out.

It announces the copy as well as drawing it. A tick that appears silently tells a sighted reader it worked and tells nobody else. The live region beside the button is the part that actually reports.

It stays reachable without a pointer. It is invisible until the block is hovered — a permanent button in the corner of every panel is clutter — and on its own that makes a control the keyboard cannot see. It is visible whenever it has focus too, and that pairing is the whole trick.

<div className="group relative rounded-md border border-line">
<pre>{payload}</pre>
<CopyButton
value={payload}
label={t('copy')}
copiedLabel={t('copied')}
className="absolute right-2 top-2"
/>
</div>

The container carries group. The button reveals on group-hover, so it appears when the pointer is anywhere over the block rather than only once it has found a button it cannot see.

Both labels are required and neither has a default. A string this component invents is a string your product cannot translate, and it would ship in English to every reader who does not read English.

Copyable answers the same wish in the opposite shape, and they are not interchangeable.

Copyable CopyButton
Where In a sentence, in a table cell In the corner of a block
Shows The text it copies An icon
Visible Always On hover, and on focus
For An id, a path, a hash Code, a payload, a column

Reach for Copyable when the value itself is the thing on screen, and this when the content is already there and the button is furniture.

Props

Read from the types in ui/copy-button.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
valuerequiredstringWhat lands on the clipboard.
labelrequiredstringWhat the button is called, for a screen reader. Required, and deliberately without a default: a string this component invents is a string the product cannot translate, and it would ship in English to every reader who does not read English.
copiedLabelrequiredstringWhat is announced after a successful copy. Required for the same reason.
onCopy(ok: boolean) => voidTold what happened, for a product that wants its own toast. false means the clipboard refused.

And every prop of React's ButtonHTMLAttributes<HTMLButtonElement> except onCopy, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 16 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/copy-button.tsx - the file a product copies - newest first.

  1. v0.28.0
    • feat a floor for pointer targets, and a way to reach it 2d397f5
  2. v0.25.0
    • feat markdown through one stylesheet, and code whose colours are yours 5f27eb8