Base components
Copy Button
BetaSince 0.2.0
Copies a value, such as an invoice ID, in one click.
import { CopyButton } from "@anantya/uix";Overview
Use a copy button beside a value people paste elsewhere, such as an invoice number, a payment link or an API key. One click copies the value, the icon turns into a check for 2 seconds, and screen readers hear "Copied".
For a copy action with a text label, such as Copy payment link, use a Button with useCopyToClipboard.
Playground
Note
The playground forCopyButton is coming soon.Examples
Right after the value it copies, named by what it copies with aria-label.
Invoice numberINV-2026-0142
Code
import { CopyButton } from "@anantya/uix";
export default function BesideValue() {
return (
<p className="flex items-center gap-1 text-sm">
<span className="text-muted-foreground">Invoice number</span>
<span className="ml-2 font-mono">INV-2026-0142</span>
<CopyButton value="INV-2026-0142" aria-label="Copy invoice number" />
</p>
);
}Two sizes: md, 36px, is the default, and sm, 32px, fits dense rows such as a table.
GSTIN 27AABCM1234F1Z5
IFSC HDFC0001234
Code
import { CopyButton } from "@anantya/uix";
export default function Sizes() {
return (
<>
<p className="flex items-center gap-1 font-mono text-sm">
GSTIN 27AABCM1234F1Z5
<CopyButton value="27AABCM1234F1Z5" aria-label="Copy GSTIN" size="sm" />
</p>
<p className="flex items-center gap-1 font-mono text-sm">
IFSC HDFC0001234
<CopyButton value="HDFC0001234" aria-label="Copy IFSC code" />
</p>
</>
);
}ghost, the default, sits quietly beside text. secondary gives it an edge, for a value the
reader is meant to copy, such as an invite link.
UPI mehtatextiles@hdfcbank
app.example.com/join/7KQ2M9XD
Code
import { CopyButton } from "@anantya/uix";
export default function Variants() {
return (
<>
<p className="flex items-center gap-1 font-mono text-sm">
UPI mehtatextiles@hdfcbank
<CopyButton value="mehtatextiles@hdfcbank" aria-label="Copy UPI ID" />
</p>
<p className="flex items-center gap-2 font-mono text-sm">
app.example.com/join/7KQ2M9XD
<CopyButton
value="https://app.example.com/join/7KQ2M9XD"
aria-label="Copy invite link"
variant="secondary"
/>
</p>
</>
);
}copiedMessage says what was copied, such as "Payment link copied", where "Copied" isn't
clear enough.
https://pay.example.com/INV-2026-0142
Code
import { CopyButton } from "@anantya/uix";
const paymentLink = "https://pay.example.com/INV-2026-0142";
export default function CopiedMessage() {
return (
<p className="flex items-center gap-1 text-sm">
<span className="font-mono break-all">{paymentLink}</span>
<CopyButton
value={paymentLink}
aria-label="Copy payment link"
copiedMessage="Payment link copied"
/>
</p>
);
}onCopy gets whether the copy worked, so you can say what to do when it doesn't, such as on a
page not served over HTTPS.
INV-2026-0142
Code
"use client";
import { CopyButton } from "@anantya/uix";
import { useState } from "react";
export default function WhenCopyingFails() {
const [failed, setFailed] = useState(false);
return (
<div className="flex flex-col items-center gap-2 text-sm">
<p className="flex items-center gap-1">
<span className="font-mono select-all">INV-2026-0142</span>
<CopyButton
value="INV-2026-0142"
aria-label="Copy invoice number"
onCopy={(copied) => setFailed(!copied)}
/>
</p>
{failed && <p>Couldn't copy it. Select the number and press Ctrl+C or ⌘C.</p>}
</div>
);
}Props
Note
The props table forCopyButton is coming soon.Accessibility
It follows the WAI-ARIA button pattern. It renders a <button type="button">, so it never submits a form.
| Key | Action |
|---|---|
| Tab | Moves focus to the next button. |
| Shift + Tab | Moves focus to the previous button. |
| Enter or Space | Copies the value and announces Copied, or Copy failed. |
Screen readers
- A screen reader announces it as a button named by its
aria-label, such as "Copy invoice number, button". Its icon is hidden from screen readers - After a copy it announces "Copied" politely, or "Copy failed" assertively, through the LiveRegion that ThemeProvider renders
- Its name stays the same after a copy, so it isn't read again
Contrast
Its icon needs 3:1, as does the focus outline. The lowest contrast at rest, on hover and while pressed, on the page and in a dialog:
| Variant | Light | Dark |
|---|---|---|
ghost | 16.1:1 | 14.2:1 |
secondary | 11.9:1 | 7.3:1 |
| Focus outline | 5.2:1 | 6.7:1 |
The check doesn't rely on color: its shape changes. With forced colors on, such as Windows high contrast, it draws in system colors.
WCAG 2.2
- 1.4.11 Non-text Contrast, in light and dark
- 2.1.1 Keyboard: Enter and Space copy
- 2.4.7 Focus Visible: a focus outline shows on keyboard focus
- 2.5.8 Target Size (Minimum): it is at least 32 by 32px
- 4.1.2 Name, Role, Value: a native button, named by its
aria-label - 4.1.3 Status Messages: the result is announced without moving focus
axe checks every example on this page in CI, in light and dark, in Chromium, Firefox and WebKit.
What your app still has to do
- Name it after what it copies, such as Copy invoice number, when "Copy" alone isn't clear, as when a page has more than one
- Keep the value on screen, so people can still select and copy it when copying fails
- Mount ThemeProvider, or a LiveRegion, so the result is announced
Do / Don't
Name it after what it copies, such as Copy invoice number.
Don't leave several on one screen named only Copy. A screen reader user can't tell them apart.
Put it right after the value it copies.
Don't put it inside a link. A click would copy and follow the link at once.
Say what to do when copying fails, with onCopy.
Don't hide the value behind the button. When copying fails, there is no other way to get it.
Changelog
Every change to Copy Button since 0.2.0 is in the release notes.