Copy button
A button that puts a value on the clipboard and says it did, labeled or as an icon alone.
On this page
Code
import { CopyButton } from "@bosunkit/components/copy-button";
import type { CSSProperties, ReactElement } from "react";
export const description: string =
"The identifiers the crew read out on the radio or type into a form, each with an icon-only copy beside it: the " +
"MMSI and call sign are copied as written, and the position as degrees and decimal minutes.";
const ROW: CSSProperties = {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "var(--space-md)",
borderBottom: "1px solid var(--border)",
};
const TERM: CSSProperties = { color: "var(--text-dim)", fontFamily: "var(--font-mono)" };
export default function Showcase(): ReactElement {
const identifiers: readonly (readonly [string, string, string])[] = [
["MMSI", "235 123 456", "235123456"],
["Call sign", "MABC7", "MABC7"],
["Position", "50° 08.52′ N 005° 03.10′ W", "50° 08.52′ N 005° 03.10′ W"],
];
return (
<div style={{ width: "100%", maxWidth: 440, display: "grid" }}>
{identifiers.map(([term, shown, value]: readonly [string, string, string]): ReactElement => (
<div key={term} style={ROW}>
<span style={TERM}>{term}</span>
<span style={{ flex: 1, textAlign: "end", fontFamily: "var(--font-mono)" }}>{shown}</span>
<CopyButton
variant="ghost"
iconOnly
value={value}
label={`Copy ${term}`}
copiedLabel="Copied"
failedLabel="Could not copy"
/>
</div>
))}
</div>
);
}
A copy button writes value to the clipboard when it is pressed and shows the result: the copy mark swaps for a check
with a short scale and fade, and the label changes with it ("Copy position", "Position copied"). If the browser
refuses, which it does when the page has no permission or no secure connection, the mark swaps for a cross and the
button says so. After resultDurationMs the button returns to its label.
The three labels sit in one cell, so the button is as wide as the longest and a result never moves what is beside it. A label changing in place is not announced, so the result is also written to a polite status line. The caller supplies all three labels, translated, so the component adds no words of its own.
Examples
Labeled
Code
import { CopyButton } from "@bosunkit/components/copy-button";
import type { ReactElement } from "react";
export const description: string =
"The labeled button, where a copy is the action of its own: the icon swaps and the label changes in place, and the " +
"button keeps the width of its longest label.";
export default function Labeled(): ReactElement {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-md)" }}>
<CopyButton
value="50° 08.52′ N 005° 03.10′ W"
label="Copy position"
copiedLabel="Position copied"
failedLabel="Could not copy"
/>
<CopyButton
variant="primary"
value="Engine hours 1,842"
label="Copy for the log"
copiedLabel="Copied"
failedLabel="Could not copy"
/>
</div>
);
}
Icon only
Code
import { CopyButton } from "@bosunkit/components/copy-button";
import type { ReactElement } from "react";
export const description: string =
"The icon-only button is a 48 px square named by its `label`, in a tooltip and to a screen reader. The name stays " +
"the same after a copy; the result is announced on its own line.";
export default function IconOnly(): ReactElement {
return (
<div style={{ display: "flex", alignItems: "center", gap: "var(--space-md)" }}>
<CopyButton iconOnly value="235123456" label="Copy MMSI" copiedLabel="Copied" failedLabel="Could not copy" />
<CopyButton
iconOnly
variant="ghost"
value="235123456"
label="Copy MMSI"
copiedLabel="Copied"
failedLabel="Could not copy"
/>
</div>
);
}
Copy the useful representation
Choose whether the copied value is a human-readable display value, a raw identifier or complete source text, and name the action accordingly. Preserve YAML indentation and authored line breaks when copying code. A report copy action should include the context needed to interpret the content rather than copying only a colored badge.
When the clipboard is unavailable, keep the original text selectable or provide a source/download route. The failed label should explain that copying failed without implying the underlying record changed. Check keyboard activation and the announced result, including repeated copies of the same value.
When the copy is refused
A page without clipboard permission refuses the copy. The button then says so, and the text appears beside it in a read-only field, selected and focused, with the keys that copy it (⌘C on Apple's platforms, Ctrl+C elsewhere), so the crew can copy it by hand. The field stays until it loses focus.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonVariant | undefined | – | The button's emphasis: primary for the main action, danger for a destructive one, ghost for a quiet one. |
| textCase | ButtonTextCase | undefined | – | How the label is cased: in capitals by default. none keeps the letters as written, for a label whose case
carries meaning, such as a chess move in SAN (Nf3, exd5), a call sign or a chart number. |
| valuerequired | string | – | The text a press puts on the clipboard: an MMSI, a call sign, a position, a part number. |
| labelrequired | string | – | What the button says, and its name for a screen reader when it is icon-only ("Copy MMSI"); translated by the caller. |
| copiedLabelrequired | string | – | What it says once the text is on the clipboard ("Copied"); translated by the caller. |
| failedLabelrequired | string | – | What it says when the browser refuses the copy ("Could not copy"); translated by the caller. |
| iconOnly | boolean | undefined | false | Whether the button is the icon alone, a square touch target that names itself in a tooltip and to a screen reader. Off by default, where the icon sits beside the label. |
| resultDurationMs | number | undefined | 2000 | How long the result ("Copied", "Could not copy") stays before the button returns to its label, in milliseconds. |
| onResult | ((status: "copied" | "failed") => void) | undefined | – | Called with the result of each press, after the button shows it. |
| motion | boolean | undefined | true | Whether the icon swaps with a short scale and fade and the label fades to the result; on by default. false
shows each at once. Under reduced motion the icon only fades, whatever this says. |
| testId | string | undefined | – | Becomes data-testid on the button; the result line is ${testId}--status. |
| ref | Ref<HTMLButtonElement> | undefined | – | The button element. |
Also takes every attribute of Omit<ButtonProps, "children" | "onClick" | "size" | "motion" | "ref">.
Related
- Button: the button a labeled copy is built on, with its emphases and sizes.
- Tooltip: the name an icon-only copy button shows.
- Description list: the terms and values a copy button usually sits beside.