Copy button
Copies a value to the clipboard and confirms it in place.
Examples
Helpdesk widget
sk_demo_0000••••••••••••tkt_8f2c41Label
hiddendefault
shown
Styles
ghostdefault
secondary
States
rest
hover
focus
disabled
fetching
copied
failed
Usage
When to use it
- Use
- The person needs a value somewhere else: an ID, a key, a canned reply, a URL.
- Use
- Selecting by hand is slow or error-prone: a 40-character key, a token, a multi-line reply.
- Not for
- The value has to be read and edited, not copied. Use Input.
- Not for
- The value is a block of code with a copy action in its corner. Use Code block.
- Not for
- The action shares a link; label it "Copy link". Use Button.
Rules
7 rules
- Must
- Pass a function that fetches a secret; never the secret itself. Because a secret passed as value sits in the page's props before anyone asks to copy it.
- Never
- Add a toast on success; the button confirms in place. Because the icon turns to a check and a status region says "Copied", so a toast says it twice.
- Must
- Pass label on an icon-only button so each one has its own name. Because without label every icon button is named "Copy", so a row of three reads the same.
- Must
- Set label to the thing copied, such as "conversation ID"; never a sentence or a verb. Because the name is "Copy {label}" and the announcement "{label} copied", so a sentence reads twice.
- Never
- Swap a Button label between Copy and Copied; use CopyButton. Because the swap changes the width and a screen reader hears nothing, where CopyButton holds its width and announces.
- Should
- Give each copyable value its own button, up to 3 per row; never one that copies a combination.
- Should
- Hide the button when the value is empty.
Accessibility
5 checks
- The button is named `Copy <label>` and sets aria-busy while fetching.
- A fixed, sr-only role="status" region announces `<label> copied` on every copy.
- A failure announces "Could not copy. Select the text and copy it by hand."
- The glyph and words are aria-hidden; the glyph shape (check, warning) carries the state without color.
- Icon only is 28px, past the 24px target.
Edge cases
12 edge cases
- 2,000-character value
- Copies all of it; truncate the value beside the button in the middle.
- Empty value, no label
- Copies an empty string and says "Copied"; hide the button.
- Inside a scroller
- The status region is fixed and sr-only, so it never adds width.
- Right to left
- Glyph and word mirror their order.
- Zoom 200%
- With showLabel the box grows with the text; icon only stays 28px.
- Tap on iOS WebKit with a fetched value
- Uses clipboard.write with a ClipboardItem promise in the same tick; falls back to writeText.
- Value takes 1.2s; never resolves
- Pends in the same box; the caller times out the fetch.
- Clipboard denied or fetch rejects
- Warning glyph and "Failed" for 1.6s.
- Ten presses in a second
- Each copies and restarts the timer; the box never moves.
- Inside an Input suffix or code block corner
- Ghost flips with the surface channels.
- Forced colors
- Button draws its ButtonText border; glyph shapes switch.
- Reduced motion
- The glyph swap is a cut; the spinner stands still.
API
7 props
valuestring | () => string | Promise<string>defaultrequiredlabelstringshowLabelbooleandefaultfalsetimeoutnumberdefault1600onCopy(value: string) => voidforceState'fetching' | 'copied' | 'failed'variant, sizeButtondefault'ghost', 'icon-sm' or 'sm'
Tokens
7 tokens
--button-ghost- transparent
--button-ghost-hover- oklch(var(--ink-c) / 0.05)
--line-control- oklch(var(--ink-c) / 0.16)
--control-sm- 28px
--line-focus- 2px
--border-focus- var(--ink)
--focus-offset- 2px