Photon Design System

Copy Button

The one copy affordance: a quiet icon button that writes value to the clipboard, swaps its glyph to a check for a moment, and says Copied to assistive tech through a polite live region. Ghost at rest so a mono value stays the loudest thing on its line. LogList, DescriptionList, and CodeBlock use it; put it after any id or key a user will paste somewhere. Give it the value as children and the value is a click target too.

pho_prj_01kztbv929fagsn0pk422y682f

Import

import { CopyButton } from "@photon-ai/pho-ui/components/copy-button";

With the value

children is the view of the value, rendered before the button. A click anywhere on it copies, the way a <label> presses its control, and the wash lifts over the button while the pointer is on either. With a view the glyph rests in description ink, so it is never louder than the value it copies. The button stays the one tab stop and the accessible name; the view reads as text.

<CopyButton value={project.id} label="project id" size="xs">
  {project.id}
</CopyButton>

Custom view

Put anything in the view. It sets a default type (the mono face, xs, primary ink, one line that truncates), and what you put in sets its own: a number in the body face, a masked key in secondary ink. value is still what goes to the clipboard, so the view can show less than it copies. A link inside keeps its own click, and dragging across the text selects it instead of copying.

+1 (415) 555-0132pho_sk_live_…9fD3
<CopyButton value={number} label="number" size="xs">
  <span className="font-sans text-base font-medium">{formatted}</span>
</CopyButton>
<CopyButton value={key.secret} label="API key" size="xs">
  <span className="text-pho-secondary">{key.masked}</span>
</CopyButton>

Sizes

Three square sizes: xs (14px icon, 24px hover wash that overflows the line) inline after a value, sm (28px, the default) in a row cell or a panel header, md (32px) in a toolbar.

<CopyButton value={project.id} label="project id" size="xs" />

Props

  • value — string — what goes to the clipboard
  • children — ReactNode — the view of the value, before the button; a click on it copies too
  • label — string (default to clipboard) — what is being copied; read as Copy {label}, then {label} copied
  • size — xs / sm / md (default sm)
  • onCopied — (value: string) => void — after the write lands
  • …plus any <button> attribute

The state behind it is useCopy() from @photon-ai/pho-ui/utils: copy(text) resolves to whether the write landed, and copied holds for COPY_FEEDBACK_MS (1.5 s) so a control of your own can swap its glyph.

Accessibility

  • Named for the thing. Copy request id, never Copy; the label is the only thing a screen reader hears.
  • Feedback in two channels. The glyph turns to a check, and a polite live region says Copied.
  • The view is a pointer shortcut. Like a <label>, it is not a tab stop; keyboard and screen reader users reach the button, named for the thing, and hear the value as the text before it.
  • Refusal is quiet. When the clipboard refuses (no permission, an insecure page), nothing changes; onCopied is not called.

Best practices

  • One per value, right after it. A row with three copy buttons is a table.
  • Pass the value as children rather than setting it beside the button, so the whole value is the target, not only the 14px glyph.
  • Copy the whole value even when the display is masked or truncated; that is what the button is for.