Skip to content
UI

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
Tenant IDtnt_8f3a2c91d4
<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.

  • value is the exact text written to the clipboard. Pass the raw value, never a masked or truncated rendering of it.
  • label is 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.

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
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.

  • copyValue adds the copy action. Omit it to drop the action.
  • onPivot adds 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 IPCountryRule
203.0.113.42NetherlandsSSH brute force
198.51.100.7BrazilImpossible travel
203.0.113.42NetherlandsPort 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 CopyButton without revealOnHover.

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.

PropTypeDefault
copiedLabelstring

Already-translated text announced to screen readers after a copy.

Copied
labelstring

Already-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.

-
revealOnHoverboolean

Hide until the enclosing `group/value` is hovered.

false
value*string

The 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.

PropTypeDefault
copyLabelstring

Already-translated label of the copy action.

-
copyValuestring | null

Text 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.

-
pivotLabelstring

Already-translated label of the pivot action.

Search for this value