Copy Button
Copies one value to the clipboard, alone or with a pivot action next to a value in a table.
@zenoxai/ui/patterns/copy-button Markdown<div className="flex items-center gap-1.5 text-sm">
<span className="text-text-secondary">Tenant ID</span>
<span className="font-mono">tnt_8f3a2c91d4</span>
<CopyButton value="tnt_8f3a2c91d4" label="Copy tenant ID" />
</div>Usage
import { CopyButton } from "@zenoxai/ui/patterns/copy-button"
export function TenantId({ id }: { id: string }) {
return (
<span className="gap-1.5 flex items-center">
<span className="font-mono">{id}</span>
<CopyButton value={id} label="Copy tenant ID" />
</span>
)
}
Put a CopyButton after values people paste somewhere else: identifiers, hashes, IP addresses,
domains, commands. After a successful copy the icon turns into a check for a moment.
valueis the exact text written to the clipboard. Pass the raw value, never a masked or truncated rendering of it.labelis the accessible name and the tooltip. It defaults to “Copy”; name what is copied (“Copy tenant ID”) whenever more than one value on screen can be copied.
In tables, wrap the value in ValueActions instead of placing buttons by hand. It adds copy and
pivot actions that appear on hover.
Examples
With a toast
The check icon is enough next to a single value. When the surface needs more feedback, or the
copy can fail (the browser denies clipboard access), raise a toast from onCopy and
onCopyError. The app mounts the Toaster once at its root; this example renders its own.
function CopyButtonWithToast() {
const hash =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
return (
<>
<div className="flex w-full max-w-md items-center gap-1.5 text-sm">
{/* Truncated at the end; the full value is in the title and the
copy action. */}
<span className="min-w-0 truncate font-mono" title={hash}>
{hash}
</span>
<CopyButton
value={hash}
label="Copy SHA-256"
onCopy={() => toast.success("SHA-256 copied to the clipboard")}
onCopyError={() =>
toast.error("Could not copy. The browser blocked clipboard access.")
}
/>
</div>
<Toaster />
</>
)
}Value actions in a table
ValueActions renders the value followed by its actions, hidden until the pointer is over the
value or an action has keyboard focus.
copyValueadds the copy action. Omit it to drop the action.onPivotadds a search action: “show every row with this value”. Omit it to drop the action.
When the cell shows a derived label, such as a translated enum or a country name resolved from a code, offer the pivot without the copy. Copying text the user cannot see, like the underlying code, is a surprise.
Clicks on the actions do not propagate, so an action inside a clickable row does not also open the row.
| Source IP | Country | Rule |
|---|---|---|
| SSH brute force | ||
| Impossible travel | ||
| Port scan |
Hover a value to copy it or filter by it.
function CopyButtonValueActions() {
const [pivot, setPivot] = React.useState<string | null>(null)
const events = [
{
id: "evt-1",
ip: "203.0.113.42",
country: "Netherlands",
rule: "SSH brute force",
},
{
id: "evt-2",
ip: "198.51.100.7",
country: "Brazil",
rule: "Impossible travel",
},
{
id: "evt-3",
ip: "203.0.113.42",
country: "Netherlands",
rule: "Port scan",
},
]
return (
<div className="flex w-full flex-col gap-3">
<Table containerLabel="Events">
<TableHeader>
<TableRow>
<TableHead>Source IP</TableHead>
<TableHead>Country</TableHead>
<TableHead>Rule</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{events.map((event) => (
<TableRow key={event.id}>
<TableCell>
<ValueActions
copyValue={event.ip}
copyLabel="Copy IP address"
onPivot={() => setPivot(`source.ip = ${event.ip}`)}
pivotLabel="Show events from this IP"
>
<span className="font-mono">{event.ip}</span>
</ValueActions>
</TableCell>
<TableCell>
{/* A derived label: pivot on it, but do not copy it. */}
<ValueActions
onPivot={() => setPivot(`country = ${event.country}`)}
pivotLabel="Show events from this country"
>
{event.country}
</ValueActions>
</TableCell>
<TableCell>{event.rule}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<p className="text-sm text-text-secondary">
{pivot
? `Filter: ${pivot}`
: "Hover a value to copy it or filter by it."}
</p>
</div>
)
}Accessibility
- Each action is a real icon button named by its label, with the same text as a tooltip.
- Hidden actions stay in the tab order and appear when they receive keyboard focus.
- The swap to a check icon is not announced to screen readers. When the copy matters to the
task, confirm it with a toast from
onCopy, which is announced. - Touch screens have no hover, so hover-revealed actions stay hidden there. Where touch
matters, render
CopyButtonwithoutrevealOnHover.
API reference
Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.
CopyButton
CopyButton copies one value to the clipboard and confirms it by swapping its icon for a check for a moment.
| Prop | Type | Default |
|---|---|---|
copiedLabel | stringAlready-translated text announced to screen readers after a copy. | Copied |
label | stringAlready-translated accessible name and tooltip. | Copy |
onCopy | ((value: string) => void)Called after the value reached the clipboard. Show a toast here when the surface needs more feedback than the check icon. | - |
onCopyError | ((error: unknown) => void)Called when the value could not be written: the page is not a secure context (no Clipboard API) or the browser denied the write. | - |
revealOnHover | booleanHide until the enclosing `group/value` is hovered. | false |
value* | stringThe exact text written to the clipboard, never a masked rendering. | - |
ValueActions
ValueActions renders a value followed by its actions, revealed on hover: copy it and pivot on it. It owns the `group/value` the actions hide behind, so the caller composes the value's own rendering as children.
| Prop | Type | Default |
|---|---|---|
copyLabel | stringAlready-translated label of the copy action. | - |
copyValue | string | nullText written to the clipboard. Omit to drop the copy action. | - |
onCopy | ((value: string) => void)Forwarded to the copy action. | - |
onPivot | (() => void) | null"Show every row with this value". Omit to drop the pivot action. | - |
pivotLabel | stringAlready-translated label of the pivot action. | Search for this value |