ComponentsButton
Copy button
Copies a value to the clipboard and confirms it with a check and "Copied".
Import
import { CopyButton } from "@booleanpress/ui/copy-button"Usage
Give CopyButton the text to copy as value, or a getValue function that returns it (or a promise of it) at the moment of the click.
import { CopyButton } from "@booleanpress/ui/copy-button"
export function ApiKey({ apiKey }: { apiKey: string }) {
return <CopyButton value={apiKey} label="Copy API key" />
}It writes with the Clipboard API, and where that is missing or refused (an insecure page, a frame without permission) with a hidden text area and the browser's copy command. On success the icon turns to a check and it says "Copied" for timeout ms (2000), in its tooltip or label and in a polite live region; onCopy gets the text. If both ways fail it shows a cross and "Copy failed", and onCopyError is called. The fallback puts its hidden text area beside the button, so it works inside a Dialog or Sheet, whose focus trap would otherwise pull focus out of it. The same writer is exported as copyText(text, container?), for copying from your own controls; pass an element inside the open dialog as container.
By default it is an icon button with a tooltip; label names what it copies. showLabel shows "Copy", then "Copied", beside the icon. size is xs (24 px, for inside an InputGroup), sm, default or lg; variant is Button's (ghost for the icon, outline with the label).
Examples
Icon only
An icon button beside a message ID; the tooltip says "Copy", then "Copied".
With label
showLabel: the text changes to "Copied" with the check.
import { CopyButton } from "@booleanpress/ui/copy-button"
export default function CopyButtonWithLabel() {
return (
<div className="flex flex-wrap items-center gap-2">
<CopyButton value="smtp.example.com" showLabel />
<CopyButton value="smtp.example.com" showLabel variant="secondary" size="sm" />
</div>
)
}In an input group
An xs copy button at the end of a read-only API key field.
import { CopyButton } from "@booleanpress/ui/copy-button"
import { InputGroup, InputGroupAddon, InputGroupInput } from "@booleanpress/ui/input-group"
import { Label } from "@booleanpress/ui/label"
const KEY = "bp_live_4f2a9c7e1d8b6053"
export default function CopyButtonInInputGroup() {
return (
<div className="flex w-full max-w-sm flex-col gap-2">
<Label htmlFor="api-key">API key</Label>
<InputGroup>
<InputGroupInput id="api-key" value={KEY} readOnly className="font-mono" />
<InputGroupAddon align="inline-end">
<CopyButton value={KEY} label="Copy API key" size="xs" />
</InputGroupAddon>
</InputGroup>
</div>
)
}Custom timeout
timeout={5000} and getValue; onCopy counts the copies.
import * as React from "react"
import { CopyButton } from "@booleanpress/ui/copy-button"
export default function CopyButtonCustomTimeout() {
const [copies, setCopies] = React.useState(0)
return (
<div className="flex flex-col items-start gap-2">
<CopyButton
showLabel
timeout={5000}
getValue={() => "v=spf1 include:_spf.example.com ~all"}
onCopy={() => setCopies((count) => count + 1)}
/>
<p className="text-sm text-muted-foreground">
The SPF record stays “Copied” for five seconds. Copied {copies} {copies === 1 ? "time" : "times"}.
</p>
</div>
)
}Disabled
disabled: the button cannot be pressed and copies nothing.
import { CopyButton } from "@booleanpress/ui/copy-button"
export default function CopyButtonDisabled() {
return (
<div className="flex items-center gap-2">
<CopyButton disabled value="bp_live_4f2a9c7e1d8b6053" label="Copy API key" />
<CopyButton disabled showLabel value="bp_live_4f2a9c7e1d8b6053" />
</div>
)
}Copy fails
A getValue that rejects: the button shows a cross and "Copy failed", and onCopyError receives the error.
import * as React from "react"
import { CopyButton } from "@booleanpress/ui/copy-button"
export default function CopyButtonCopyFails() {
const [error, setError] = React.useState("")
return (
<div className="flex flex-col items-start gap-2">
<CopyButton
showLabel
// The value cannot be read, so nothing reaches the clipboard: the button shows a cross and says "Copy failed".
getValue={() => Promise.reject(new Error("The backup code could not be read."))}
onCopyError={(reason) => setError(reason instanceof Error ? reason.message : "Copy failed")}
/>
<p className="min-h-5 text-sm text-destructive-strong">{error}</p>
</div>
)
}Accessibility
- Semantics
- A native
button, followed by a visually hidden<output>(a polite status region) that says "Copied" or "Copy failed" after a press. - Labels
- The icon button is named by
label, or the provider'scopystring; its name does not change when it has copied. WithshowLabel, the visible "Copy" or "Copied" is the name. "Copied" and "Copy failed" are the provider'scopiedandcopyFailedstrings. - Focus
- Focus stays on the button through the copy, the fallback included, inside a dialog too. The tooltip opens on hover and keyboard focus, and stays open with "Copied" while the check shows, until Escape closes it.
- Known limits
- The Clipboard API needs a secure page (HTTPS or localhost); the fallback depends on the browser still supporting the copy command.
- Screen readers announce "Copied" once per copy; two presses within the timeout are announced once.
Keyboard
| Key | Behaviour |
|---|---|
| Enter | Copies the value. |
| Space | Copies the value. |
| Escape | Closes the tooltip, "Copied" included. |
API
CopyButton
| Prop | Type | Default | Description |
|---|---|---|---|
getValue | (() => string | Promise<string>) | Returns the text to copy at the moment of the click, for a value that is not known while rendering. Wins over value. | |
label | string | The icon-only button's accessible name, naming what it copies ("Copy API key"). The provider's copy string by default. | |
loading | boolean | Shows the spinner in place of the leading icon, sets aria-busy and aria-disabled and ignores presses (a click,
Enter, Space, a form's submission), while the button keeps its focus and its place in the tab order. With
asChild the child (a link) gets the same. | |
onCopy | ((text: string) => void) | Called with the copied text once it is on the clipboard. | |
onCopyError | ((error: unknown) => void) | Called when neither the Clipboard API nor the fallback could copy. | |
raised | boolean | null | ||
rounded | boolean | null | A circle (a pill with the label). | |
severity | "success" | "info" | "warning" | "help" | "danger" | "contrast" | null | The colour of the default, outline, ghost and link variants, as Button's. | |
showLabel | boolean | false | Shows "Copy" (then "Copied") beside the icon instead of the icon alone. |
size | "default" | "xs" | "sm" | "lg" | xs 24 px, sm 28 px, default, lg 42 px. The provider's controlSize when left out. | |
timeout | number | 2000 | Milliseconds the check and "Copied" stay. 2000 by default. |
tooltip | boolean | true | The icon-only button's tooltip: "Copy", then "Copied". true by default. |
tooltipSide | "top" | "bottom" | "left" | "right" | top | The tooltip's side: top (default), right, bottom or left. |
value | string | The text to copy. | |
variant | "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | The emphasis, as Button's: ghost for the icon button, outline with the label, by default. |
Also exported: copyText, a helper the parts use.
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="copy-button" (CopyButton), and data-state.
Provider strings: copied, copyFailed, copy (BooleanUIProvider's strings).
Theming
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|