|Kit

Clipboard

Copy-to-clipboard building blocks on Ark UI's Clipboard machine — the one-line Copy icon button, and the full Clipboard compound for a read-only "value + copy button" field.

Two ways to copy a value to the clipboard, both on top of Ark UI's Clipboard machine:

  • Copy — a one-line icon button. No visible value, just an ActionIcon that swaps to a checkmark on copy. The common case (a share link's copy button, an API key's copy affordance next to other text).
  • Clipboard — the full compound (Root / Label / Control / Input / Trigger / Indicator / ValueText), for a read-only field that shows the value and a copy button.
import { Copy } from '@42-central/ui-react/clipboard';

<Copy value="https://example.com/s/abc123" />;

Playground

Component

Loading preview

Props

Code

<Component value="https://example.com/s/abc123" timeout={3000} />

Basic usage

value is the only required prop. The icon swaps to a checkmark on copy and reverts to the copy icon after timeout.

Code

<Copy value="https://example.com/s/abc123" />

Variants, sizes, colors & radius

Copy forwards variant / size / radius / color straight to ActionIcon — see its own docs for the full (variant × color) model. There's no separate styling surface to learn.

Code

<Copy value="…" variant="filled" color="brand" />
<Copy value="…" variant="light" color="teal" />
<Copy value="…" variant="outline" color="blue" />
<Copy value="…" variant="subtle" color="neutral" />
<Copy value="…" variant="filled" color="brand" radius="full" size="lg" />

Timeout

timeout controls how long (in ms) the checkmark stays before reverting to the copy icon. Ark's own default is 3000.

Code

<Copy value="…" timeout={1000} />

onCopiedChange

Fires true once the value is copied — flattened from Ark's own onStatusChange. It only fires on copy, never on the automatic revert to idle after timeout (that's Ark's own machine behavior, not a gap in this wrapper) — read the live state off Clipboard.Indicator if you need it.

<Copy
  value="https://example.com/s/abc123"
  onCopiedChange={(copied) => copied && track('share_link_copied')}
/>

By default Copy is a closed leaf preset: it owns its ActionIcon outright and doesn't accept asChild or a custom aria-label — the accessible name comes from Ark's own "Copy to clipboard" / "Copied to clipboard" translations. For a different trigger element entirely, reach for the headless mode below, or compose Clipboard.Root / Clipboard.Trigger directly (further down) for translated text on the default element shape.

Headless

Pass a function as children instead of omitting it to go fully headless: Copy renders none of its own markup — no ActionIcon, no icon swap — and instead calls the function with { copied, copy }, so you render whatever trigger you want and wire copy to its onClick yourself. copied and copy still ride the same timeout / onCopiedChange behavior as the default button.

Code

<Copy value="https://example.com/s/abc123">
  {({ copied, copy }) => (
    <button type="button" onClick={copy}>
      {copied ? 'Copied!' : 'Copy link'}
    </button>
  )}
</Copy>

Copy doesn't attach a click handler to your returned JSX on its own — copy is only wired up where you put it, so a non-interactive render (a <span>, plain text) stays non-interactive. variant / size / radius / color / testId only apply to the default ActionIcon rendering — in headless mode they're unused, since there's no ActionIcon to forward them to.

The Clipboard field

For a read-only "value + copy button" field — share links, API keys, invite codes — compose the parts directly. Clipboard.Input renders the value in the shared InputBase shell (always read-only); the Trigger sits inside it as a real, focusable sibling — not through the shell's decorative endSlot, which would hide an interactive button from assistive tech.

Code

<Clipboard.Root defaultValue="https://example.com/s/abc123">
  <Clipboard.Label>Share link</Clipboard.Label>
  <Clipboard.Input>
    <Clipboard.Trigger asChild>
      <ActionIcon variant="subtle" color="neutral" size="sm" aria-label="Copy link">
        <Clipboard.Indicator copied={<CheckIcon />}>
          <CopyIcon />
        </Clipboard.Indicator>
      </ActionIcon>
    </Clipboard.Trigger>
  </Clipboard.Input>
</Clipboard.Root>

Clipboard is parts-only — there's no bare-invocation convenience form, the same shape as Popover. A rich field like this isn't a single-prop drop-in the way Copy is.

Controlled value

value / onChange (flattened from Ark's onValueChange) make it controlled, same as any other value-bearing kit component.

Code

const [value, setValue] = useState('https://example.com/s/abc123');

<Clipboard.Root value={value} onChange={setValue}>
  <Clipboard.Input>
    <Clipboard.Trigger asChild>
      <ActionIcon variant="subtle" color="neutral" size="sm" aria-label="Copy link">
        <Clipboard.Indicator copied={<CheckIcon />}>
          <CopyIcon />
        </Clipboard.Indicator>
      </ActionIcon>
    </Clipboard.Trigger>
  </Clipboard.Input>
</Clipboard.Root>

<Button size="xs" variant="light" onClick={() => setValue(regenerate())}>
  Generate new link
</Button>

Anatomy

  • Clipboard.Root — owns the value + copied/idle state; renders the real <div> the rest of the compound reads context from.
  • Clipboard.Label — Ark's raw Label part, a real <label>. Unstyled by default — size and style it like any other field label.
  • Clipboard.Control — Ark's raw Control part; carries data-copied. Only needed if you want a different shell than Clipboard.Input's built-in InputBase.
  • Clipboard.Input — the value, in the shared InputBase shell (always read-only). Pass the Trigger as children so it renders as a real sibling, not through the shell's aria-hidden endSlot.
  • Clipboard.Trigger — Ark's raw Trigger part, a real <button>. asChild to swap in ActionIcon (or any other trigger element).
  • Clipboard.Indicator — Ark's raw Indicator part. Pass copied={<CheckIcon />} to swap content once the value is copied; it reverts once the state returns to idle.
  • Clipboard.ValueText — Ark's raw ValueText part; renders the live value as plain text instead of a form control.

testId

Clipboard.Root renders a real <div> and takes its own testId. Clipboard.Input is a shell — like Input — so its testId lands on the real <input>, not the shell <div>. Copy's testId lands on its ActionIcon's underlying <button>. Clipboard.Label / Control / Trigger / Indicator / ValueText are raw Ark re-exports and forward an arbitrary data-testid straight through natively. See Testing.

API

Copy

Prop

Type

Clipboard.Root

Prop

Type

Clipboard.Input

Prop

Type

Accessibility

  • Copy's accessible name comes from Ark's own translated "Copy to clipboard" / "Copied to clipboard" trigger label — announced by screen readers as the copied state changes.
  • In the Clipboard compound, Clipboard.Trigger gets that same automatic aria-label for free; override it via Clipboard.Root's translations prop.
  • Clipboard.Input's Trigger is rendered as a real, focusable sibling of the input — not through InputBase's decorative endSlot (which is aria-hidden) — so it stays reachable by keyboard and announced to assistive tech.
  • Clipboard.Indicator's content swap is driven by the machine's own live state, so assistive tech announcing the trigger's aria-label change is the primary signal — the icon swap itself is decorative.
  • In Copy's headless mode, none of the above is automatic — the rendered element, its accessible name/label, and the onClick wiring are entirely yours, same as composing Clipboard.Root / Clipboard.Trigger directly.

Changelog

v0.19.0
  • These entry points ship "use client", so a Server Component can import them

    #320
v0.18.0
  • Clipboard.Input asChild wires your input as the read-only value field

    #288
v0.8.0
  • New Clipboard component, with a ready-made Copy that also takes a headless render prop

    #119

On this page