Copy button
StableCopy text to the clipboard with one press: a pearl button that says “Copied”, a quiet one for code, and the helper under both.
CopyButton is the marketing version: a small pearl button whose words change to “Copied” with a check. CopyCodeButton is the one inside every code block: its name never changes, the result is announced, and it explains what to do if the browser blocks the clipboard. copyText() is the helper for your own controls.
Updated View source
Two copy buttons
import { CopyButton } from '@rchursin/home/ui'import { SampleBadge } from '@rchursin/product/shell/layout'import { CopyCodeButton } from '@rchursin/product/ui'
/** * Both copy for real. CopyButton (pearl) changes its words to "Copied" for 1.6 seconds. * CopyCodeButton (ghost) keeps its name, shows a check for 2 seconds and announces the result. */const inviteLink = 'https://example.com/invite/k7q2-m9xd'const projectId = 'prj_4fd81c2e'
export default function CopyButtonBasic() { return ( <div className="lux-card w-full max-w-md rounded-2xl bg-raised"> <div className="flex items-center justify-between gap-3 border-b border-line px-4 py-3"> <h3 className="text-[14px] font-semibold tracking-[-0.01em] text-fg">Share project</h3> <SampleBadge /> </div> <ul className="flex flex-col divide-y divide-line"> <li className="flex items-center gap-3 px-4 py-3"> <div className="flex min-w-0 flex-1 flex-col gap-0.5"> <span className="text-[12px] text-fg-3">Invite link</span> <span className="truncate font-mono text-[12.5px] text-fg">{inviteLink}</span> </div> <CopyButton text={inviteLink} label="Copy link" /> </li> <li className="flex items-center gap-3 px-4 py-3"> <div className="flex min-w-0 flex-1 flex-col gap-0.5"> <span className="text-[12px] text-fg-3">Project ID</span> <span className="truncate font-mono text-[12.5px] text-fg">{projectId}</span> </div> <CopyCodeButton text={projectId} what="project ID" /> </li> </ul> </div> )}Usage
import { CopyButton } from '@rchursin/home/ui'import { CopyCodeButton, copyText } from '@rchursin/product/ui'<CopyButton text="npm create vite@latest" label="Copy command" /><CopyCodeButton text={source} what="button.tsx" />const ok = await copyText(link)Examples
On a black panel
tone="glass" draws an outline button in white, for terminals and code panels that stay black in both themes.
import { CopyButton } from '@rchursin/home/ui'
/** tone="glass" for panels that stay black in both themes, like a terminal line. */const command = 'npm create vite@latest my-app'
export default function CopyButtonGlass() { return ( <div className="flex w-full max-w-md items-center gap-3 rounded-xl bg-[#0a0a0a] py-2 pr-2 pl-4 font-mono text-[13px] text-neutral-100 shadow-[inset_0_1px_0_rgb(255_255_255/0.09),0_0_0_1px_rgb(255_255_255/0.08)]"> <span aria-hidden="true" className="text-neutral-400 select-none"> $ </span> <code className="min-w-0 flex-1 truncate">{command}</code> <CopyButton text={command} label="Copy command" tone="glass" /> </div> )}copyText in your own button
It resolves to true or false, so the result can be a toast, a status line or anything else.
import { Button, copyText, useToast } from '@rchursin/product/ui'
/** * copyText() is the part under CopyCodeButton, for your own controls. It resolves to true or * false, so you decide what to say: here, a toast. The address stays on screen, so it can still * be selected by hand if the browser blocks the clipboard. */const reportLink = 'https://example.com/reports/2026-10-12'
export default function CopyTextExample() { const toast = useToast()
async function copyLink() { const ok = await copyText(reportLink) toast(ok ? { title: 'Link copied', icon: 'check' } : { title: 'Could not copy the link', description: 'Select the address and press Ctrl C or ⌘ C.', icon: 'alert' }) }
return ( <div className="flex w-full max-w-md flex-col items-center gap-3"> <p className="max-w-full truncate font-mono text-[12.5px] text-fg-2">{reportLink}</p> <Button variant="secondary" icon="link" onClick={copyLink}> Copy report link </Button> </div> )}API
Copy button props
| Prop | Type | Default | Description |
|---|---|---|---|
textrequired | string | – | What gets copied. |
label | string | 'Copy' | The button’s words before copying, e.g. “Copy command”. They change to “Copied” for 1.6 seconds. |
tone | 'paper' | 'glass' | 'paper' | paper: pearl metal for light or dark pages. glass: a white outline button for panels that are always black. |
className | string | – | Extra classes on the button. |
CopyCodeButton props
| Prop | Type | Default | Description |
|---|---|---|---|
textrequired | string | – | What gets copied. |
what | string | 'code' | Names the thing in the accessible name and announcement: “Copy button.tsx”, “Copied button.tsx”. |
label | string | 'Copy' | The visible verb, and the first word of the accessible name. |
className | string | – | Extra classes on the button. |
copyText()
| Prop | Type | Default | Description |
|---|---|---|---|
textrequired | string | – | What to copy. |
returns | Promise<boolean> | – | true once copied. Tries the Clipboard API, then a hidden textarea with execCommand("copy") (putting focus back where it was afterwards), then gives up with false. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| EnterSpace | Copies the text. |
- CopyButton: the label sits in a polite live region, so the change to “Copied” is read out. The button’s name also changes to “Copied” for 1.6 seconds, then back.
- CopyButton copies with copyText. If the browser still refuses (for example on a page not served over https), it selects the same words where they are shown nearby and reads “Press ⌘C” (“Press Ctrl C” off Apple devices) for 4 seconds, with a hidden explanation; if it can’t find them on the page it reads “Can’t copy”.
- CopyCodeButton: the accessible name stays “Copy code” (or “Copy button.tsx”) so it never changes under the person; the visible word is hidden from screen readers.
- CopyCodeButton announces “Copied button.tsx”, or “Could not copy. Select the text and press Ctrl C.” (⌘ C on Apple devices) if it failed, through the page announcer. Wrap the app in AnnouncerProvider or the announcement is silent.
- On failure CopyCodeButton shows an alert icon and “Press Ctrl+C” (“Press ⌘C” on Apple devices) for 2 seconds. Unlike CopyButton it doesn’t select the text for you.
Guidelines
Do
- Name what gets copied: “Copy link”, “Copy command”, what="project ID".
- Put the button right after the text it copies.
- Show the text too, so people can select it themselves.
Don’t
- Don’t copy something different from what is shown.
- Don’t rely on copying alone for secrets; offer a download or a reveal too.
In use
Used on this site
Related
- Code blockHighlighted code in five grey roles, with line numbers, picked-out lines, a Wrap toggle, Copy, and long files folded.
- Command lineA one-line shell command with a $ prompt and a Copy command button.
- ToastShort past-tense results with at most one action, like “Run archived · Undo”. Read out politely; they wait while hovered or focused.
- Screen-reader announcementsuseAnnounce makes screen readers say a result like “Moved to Done”, politely or, for urgent things, at once. Nothing shows on screen.
Source
The component’s own files, exactly as they run on this site.
/** * Small building blocks shared by the four home pages. * All black-and-white; every state is told apart by shape or text, never by colour alone. */import { useEffect, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type MouseEvent, type ReactNode,} from "react"import { cx } from "../components/cx"import { useIsApple } from "../product/lib/hotkeys"import { copyText } from "../product/ui/code"import type { Verdict } from "./content"
/** Stagger index for .lux-reveal children (each step is 70ms) */export const delay = (d: number) => ({ "--d": d }) as CSSProperties