# Clipboard (/docs/clipboard)





Two ways to copy a value to the clipboard, both on top of [Ark UI's Clipboard](https://ark-ui.com/) 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.

```tsx
import { Copy } from '@42-central/ui-react/clipboard';

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

## Playground [#playground]

<story.Playground />

## Basic usage [#basic-usage]

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

<Preview align="center" code="`<Copy value=&#x22;https://example.com/s/abc123&#x22; />`">
  <Copy value="https://example.com/s/abc123" />
</Preview>

## Variants, sizes, colors & radius [#variants-sizes-colors--radius]

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

<Preview align="center" code="&#x22;<Copy value=\&#x22;…\&#x22; variant=\&#x22;filled\&#x22; color=\&#x22;brand\&#x22; />\n<Copy value=\&#x22;…\&#x22; variant=\&#x22;light\&#x22; color=\&#x22;teal\&#x22; />\n<Copy value=\&#x22;…\&#x22; variant=\&#x22;outline\&#x22; color=\&#x22;blue\&#x22; />\n<Copy value=\&#x22;…\&#x22; variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; />\n<Copy value=\&#x22;…\&#x22; variant=\&#x22;filled\&#x22; color=\&#x22;brand\&#x22; radius=\&#x22;full\&#x22; size=\&#x22;lg\&#x22; />&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <Copy value="https://example.com/s/abc123" variant="filled" color="brand" />

    <Copy value="https://example.com/s/abc123" variant="light" color="teal" />

    <Copy value="https://example.com/s/abc123" variant="outline" color="blue" />

    <Copy value="https://example.com/s/abc123" variant="subtle" color="neutral" />

    <Copy value="https://example.com/s/abc123" variant="filled" color="brand" radius="full" size="lg" />
  </div>
</Preview>

## Timeout [#timeout]

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

<Preview align="center" code="`<Copy value=&#x22;…&#x22; timeout={1000} />`">
  <Copy value="https://example.com/s/abc123" timeout="1000" />
</Preview>

## `onCopiedChange` [#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.

```tsx
<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 [#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.

<Preview align="center" code="&#x22;<Copy value=\&#x22;https://example.com/s/abc123\&#x22;>\n  {({ copied, copy }) => (\n    <button type=\&#x22;button\&#x22; onClick={copy}>\n      {copied ? 'Copied!' : 'Copy link'}\n    </button>\n  )}\n</Copy>&#x22;">
  <CopyHeadlessExample />
</Preview>

`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 [#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`](/docs/input) 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.

<Preview align="center" code="&#x22;<Clipboard.Root defaultValue=\&#x22;https://example.com/s/abc123\&#x22;>\n  <Clipboard.Label>Share link</Clipboard.Label>\n  <Clipboard.Input>\n    <Clipboard.Trigger asChild>\n      <ActionIcon variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; size=\&#x22;sm\&#x22; aria-label=\&#x22;Copy link\&#x22;>\n        <Clipboard.Indicator copied={<CheckIcon />}>\n          <CopyIcon />\n        </Clipboard.Indicator>\n      </ActionIcon>\n    </Clipboard.Trigger>\n  </Clipboard.Input>\n</Clipboard.Root>&#x22;">
  <ClipboardShareLinkExample />
</Preview>

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

## Controlled value [#controlled-value]

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

<Preview align="center" code="&#x22;const [value, setValue] = useState('https://example.com/s/abc123');\n\n<Clipboard.Root value={value} onChange={setValue}>\n  <Clipboard.Input>\n    <Clipboard.Trigger asChild>\n      <ActionIcon variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; size=\&#x22;sm\&#x22; aria-label=\&#x22;Copy link\&#x22;>\n        <Clipboard.Indicator copied={<CheckIcon />}>\n          <CopyIcon />\n        </Clipboard.Indicator>\n      </ActionIcon>\n    </Clipboard.Trigger>\n  </Clipboard.Input>\n</Clipboard.Root>\n\n<Button size=\&#x22;xs\&#x22; variant=\&#x22;light\&#x22; onClick={() => setValue(regenerate())}>\n  Generate new link\n</Button>&#x22;">
  <ClipboardControlledExample />
</Preview>

## Anatomy [#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` [#testid]

`Clipboard.Root` renders a real `<div>` and takes its own `testId`. `Clipboard.Input` is a shell — like [`Input`](/docs/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](/docs/testing).

## API [#api]

### `Copy` [#copy]

<TypeTable
  type="{
  value: {
    type: 'string',
    required: true,
    description: 'The value copied to the clipboard on click.',
  },
  timeout: {
    type: 'number',
    default: '3000',
    description: &#x22;Ms before the copied state reverts to idle. Ark's own default.&#x22;,
  },
  onCopiedChange: {
    type: '(copied: boolean) => void',
    description: &#x22;Flattened from Ark's onStatusChange. Fires true on copy only, never on the automatic revert.&#x22;,
  },
  variant: {
    type: &#x22;'filled' | 'light' | 'outline' | 'subtle' | 'default'&#x22;,
    default: &#x22;'default'&#x22;,
    description: &#x22;Forwarded to ActionIcon.&#x22;,
  },
  color: {
    type: 'Color',
    description: 'Forwarded to ActionIcon.',
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: 'Forwarded to ActionIcon.',
  },
  radius: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'&#x22;,
    default: &#x22;'md'&#x22;,
    description: 'Forwarded to ActionIcon.',
  },
  testId: {
    type: 'string',
    description: 'data-testid on the ActionIcon\'s underlying <button>.',
  },
  children: {
    type: '(state: { copied: boolean; copy: () => void }) => ReactNode',
    description: 'Renders headless: called with the live clipboard state instead of the default ActionIcon. Omit for the default icon button.',
  },
}"
/>

### `Clipboard.Root` [#clipboardroot]

<TypeTable
  type="{
  value: {
    type: 'string',
    description: 'Controlled value.',
  },
  defaultValue: {
    type: 'string',
    description: 'Uncontrolled initial value.',
  },
  onChange: {
    type: '(value: string) => void',
    description: &#x22;Flattened from Ark's onValueChange.&#x22;,
  },
  onCopiedChange: {
    type: '(copied: boolean) => void',
    description: &#x22;Flattened from Ark's onStatusChange. Fires true on copy only, never on the automatic revert — read Clipboard.Indicator for the live state.&#x22;,
  },
  timeout: {
    type: 'number',
    default: '3000',
    description: &#x22;Ms before the copied state reverts to idle.&#x22;,
  },
  translations: {
    type: '{ triggerLabel?: (copied: boolean) => string }',
    description: &#x22;Overrides Ark's built-in Trigger aria-label text. English by default.&#x22;,
  },
  testId: {
    type: 'string',
    description: 'data-testid on the root <div>.',
  },
}"
/>

### `Clipboard.Input` [#clipboardinput]

<TypeTable
  type="{
  variant: {
    type: &#x22;'default' | 'unstyled'&#x22;,
    default: &#x22;'default'&#x22;,
    description: 'Shell treatment — same scale as Input.',
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: 'Height step — same scale as Input.',
  },
  children: {
    type: 'ReactNode',
    description: 'The copy Trigger, rendered as a real sibling of the input.',
  },
  classNames: {
    type: '{ root?, input? }',
    description: 'Per-part class overrides.',
  },
  testId: {
    type: 'string',
    description: 'data-testid on the real <input>, not the shell.',
  },
}"
/>

## Accessibility [#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.
