Skip to the content

ComponentsButton

Copy button

Copies a value to the clipboard and confirms it with a check and "Copied".

Import

import { CopyButton } from "@booleanpress/ui/copy-button"

Usage

Give CopyButton the text to copy as value, or a getValue function that returns it (or a promise of it) at the moment of the click.

import { CopyButton } from "@booleanpress/ui/copy-button"

export function ApiKey({ apiKey }: { apiKey: string }) {
  return <CopyButton value={apiKey} label="Copy API key" />
}

It writes with the Clipboard API, and where that is missing or refused (an insecure page, a frame without permission) with a hidden text area and the browser's copy command. On success the icon turns to a check and it says "Copied" for timeout ms (2000), in its tooltip or label and in a polite live region; onCopy gets the text. If both ways fail it shows a cross and "Copy failed", and onCopyError is called. The fallback puts its hidden text area beside the button, so it works inside a Dialog or Sheet, whose focus trap would otherwise pull focus out of it. The same writer is exported as copyText(text, container?), for copying from your own controls; pass an element inside the open dialog as container.

By default it is an icon button with a tooltip; label names what it copies. showLabel shows "Copy", then "Copied", beside the icon. size is xs (24 px, for inside an InputGroup), sm, default or lg; variant is Button's (ghost for the icon, outline with the label).

Examples

Icon only

An icon button beside a message ID; the tooltip says "Copy", then "Copied".

import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonIconOnly() {
  return (
    <div className="flex items-center gap-2 text-sm">
      <span className="font-mono">msg_01J9X4T2QZ</span>
      <CopyButton value="msg_01J9X4T2QZ" label="Copy message ID" size="sm" />
    </div>
  )
}

With label

showLabel: the text changes to "Copied" with the check.

import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonWithLabel() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <CopyButton value="smtp.example.com" showLabel />
      <CopyButton value="smtp.example.com" showLabel variant="secondary" size="sm" />
    </div>
  )
}

In an input group

An xs copy button at the end of a read-only API key field.

import { CopyButton } from "@booleanpress/ui/copy-button"
import { InputGroup, InputGroupAddon, InputGroupInput } from "@booleanpress/ui/input-group"
import { Label } from "@booleanpress/ui/label"

const KEY = "bp_live_4f2a9c7e1d8b6053"

export default function CopyButtonInInputGroup() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <Label htmlFor="api-key">API key</Label>
      <InputGroup>
        <InputGroupInput id="api-key" value={KEY} readOnly className="font-mono" />
        <InputGroupAddon align="inline-end">
          <CopyButton value={KEY} label="Copy API key" size="xs" />
        </InputGroupAddon>
      </InputGroup>
    </div>
  )
}

Custom timeout

timeout={5000} and getValue; onCopy counts the copies.

import * as React from "react"
import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonCustomTimeout() {
  const [copies, setCopies] = React.useState(0)

  return (
    <div className="flex flex-col items-start gap-2">
      <CopyButton
        showLabel
        timeout={5000}
        getValue={() => "v=spf1 include:_spf.example.com ~all"}
        onCopy={() => setCopies((count) => count + 1)}
      />
      <p className="text-sm text-muted-foreground">
        The SPF record stays “Copied” for five seconds. Copied {copies} {copies === 1 ? "time" : "times"}.
      </p>
    </div>
  )
}

Disabled

disabled: the button cannot be pressed and copies nothing.

import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonDisabled() {
  return (
    <div className="flex items-center gap-2">
      <CopyButton disabled value="bp_live_4f2a9c7e1d8b6053" label="Copy API key" />
      <CopyButton disabled showLabel value="bp_live_4f2a9c7e1d8b6053" />
    </div>
  )
}

Copy fails

A getValue that rejects: the button shows a cross and "Copy failed", and onCopyError receives the error.

import * as React from "react"
import { CopyButton } from "@booleanpress/ui/copy-button"

export default function CopyButtonCopyFails() {
  const [error, setError] = React.useState("")

  return (
    <div className="flex flex-col items-start gap-2">
      <CopyButton
        showLabel
        // The value cannot be read, so nothing reaches the clipboard: the button shows a cross and says "Copy failed".
        getValue={() => Promise.reject(new Error("The backup code could not be read."))}
        onCopyError={(reason) => setError(reason instanceof Error ? reason.message : "Copy failed")}
      />
      <p className="min-h-5 text-sm text-destructive-strong">{error}</p>
    </div>
  )
}

Accessibility

Semantics
A native button, followed by a visually hidden <output> (a polite status region) that says "Copied" or "Copy failed" after a press.
Labels
The icon button is named by label, or the provider's copy string; its name does not change when it has copied. With showLabel, the visible "Copy" or "Copied" is the name. "Copied" and "Copy failed" are the provider's copied and copyFailed strings.
Focus
Focus stays on the button through the copy, the fallback included, inside a dialog too. The tooltip opens on hover and keyboard focus, and stays open with "Copied" while the check shows, until Escape closes it.
Known limits
  • The Clipboard API needs a secure page (HTTPS or localhost); the fallback depends on the browser still supporting the copy command.
  • Screen readers announce "Copied" once per copy; two presses within the timeout are announced once.

Keyboard

Keyboard
KeyBehaviour
EnterCopies the value.
SpaceCopies the value.
EscapeCloses the tooltip, "Copied" included.

API

CopyButton

CopyButton props
PropTypeDefaultDescription
getValue(() => string | Promise<string>)Returns the text to copy at the moment of the click, for a value that is not known while rendering. Wins over value.
labelstringThe icon-only button's accessible name, naming what it copies ("Copy API key"). The provider's copy string by default.
loadingbooleanShows the spinner in place of the leading icon, sets aria-busy and aria-disabled and ignores presses (a click, Enter, Space, a form's submission), while the button keeps its focus and its place in the tab order. With asChild the child (a link) gets the same.
onCopy((text: string) => void)Called with the copied text once it is on the clipboard.
onCopyError((error: unknown) => void)Called when neither the Clipboard API nor the fallback could copy.
raisedboolean | null
roundedboolean | nullA circle (a pill with the label).
severity"success" | "info" | "warning" | "help" | "danger" | "contrast" | nullThe colour of the default, outline, ghost and link variants, as Button's.
showLabelbooleanfalseShows "Copy" (then "Copied") beside the icon instead of the icon alone.
size"default" | "xs" | "sm" | "lg"xs 24 px, sm 28 px, default, lg 42 px. The provider's controlSize when left out.
timeoutnumber2000Milliseconds the check and "Copied" stay. 2000 by default.
tooltipbooleantrueThe icon-only button's tooltip: "Copy", then "Copied". true by default.
tooltipSide"top" | "bottom" | "left" | "right"topThe tooltip's side: top (default), right, bottom or left.
valuestringThe text to copy.
variant"link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | nullThe emphasis, as Button's: ghost for the icon button, outline with the label, by default.

Also exported: copyText, a helper the parts use.

Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.

Data attributes: data-slot="copy-button" (CopyButton), and data-state.

Provider strings: copied, copyFailed, copy (BooleanUIProvider's strings).

Theming

The tokens its classes read. Change their values in your theme, and it follows.

Theme tokens
TokenUsed for