Buttons
Copy Button
A ghost icon or label button that writes text to the clipboard and flashes a timed "Copied" confirmation.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/copy-button.jsonPrompt
Build a React + TypeScript + Tailwind "CopyButton" component (lucide-react icons,
a cn() utility for className merging, no other dependencies).
Contract
- Export a forwardRef button component extending React.ButtonHTMLAttributes<HTMLButtonElement>.
- Props: `text: string` (the content written to the clipboard), `resetDelay?: number`
(default 2000ms), `variant?: "icon" | "label"` (default "icon" — "icon" renders an
icon-only square button, "label" renders an icon plus a "Copy" / "Copied" word),
and `onCopied?: () => void` fired once after a successful write.
Behavior
- On click, guard for `navigator.clipboard.writeText` existing (SSR, insecure origin,
unsupported browser). If missing, do nothing and stay idle — never throw.
- On a successful write: flip to a "copied" state (swap the Copy icon for a Check
icon; in the "label" variant, swap the word "Copy" for "Copied" in sync), call
`onCopied`, and start a timer that reverts to idle after `resetDelay`.
- Clicking again while already copied clears and restarts the timer instead of
stacking multiple timers.
- On a rejected write (permission denied, clipboard blocked): stay idle silently,
no error is thrown or surfaced.
- The reset timer is cleared on unmount so no `setState` fires after the button
is gone. Every state update happens inside the clipboard promise's resolve
callback or the timer callback — never synchronously during render or in a
bare mount effect.
- The "copied" transition is announced to assistive tech via a visually-hidden
`aria-live="polite"` region ("Copied"), independent of the visible icon/label swap.
- The "icon" variant carries `aria-label="Copy to clipboard"` on the button itself
(there is no visible text to serve as the accessible name); the "label" variant
doesn't need one — its visible text already names the button.
Rendering & styling
- Ghost styling: `border bg-transparent hover:bg-muted rounded-md`; the "icon"
variant is a `size-8` square. All colors are semantic tokens — no hex/oklch.
Merge the consumer's className via `cn()`.
- When copied, the Check icon is tinted with `var(--chart-2)` via inline style
(not a hardcoded green).
- The icon swap has a small scale-in pop (`prefers-reduced-motion` disables it —
the icon still swaps instantly, only the transition is removed).
- Focus-visible ring + `disabled:pointer-events-none disabled:opacity-50` like
any other button.
Customization levers
- `resetDelay` — how long the "copied" state holds before falling back to idle.
- `variant` — swap "icon" for "label" when the button needs to read as an
explicit "Copy" action rather than a bare icon (e.g. next to a code block vs.
inside a dense toolbar).
- Colors/icon — swap the `var(--chart-2)` copied tint for another chart token,
or swap the Check/Copy icon pair for a different lucide pair.
- Feedback — wire `onCopied` to a toast (`sonner`) if the button needs a more
prominent confirmation than the icon swap + `aria-live` announcement.
- Relationship to `use-copy-to-clipboard`: this button is the batteries-included,
drop-in version — pick it when a ghost icon/label button is exactly what you
need. Reach for the `use-copy-to-clipboard` hook instead when the UI around
the copy action doesn't look like a button at all (e.g. a highlighted row, a
custom toast, a non-button trigger) and you need to build that UI yourself.Concepts
- Icon-swap feedback — the Copy icon is replaced by a Check icon on success instead of adding a separate confirmation element, so the button itself is the feedback surface.
- Timed state with restart-on-repeat — the "copied" state is timer-backed, not a one-shot flag; clicking again while already copied restarts the timer instead of letting it flicker back to idle mid-read.
- aria-live announcement decoupled from visuals — a visually-hidden
aria-live="polite"region announces "Copied" so screen-reader users get the same feedback sighted users get from the icon swap, without duplicating the visible label. - Fail silent, stay idle — a rejected or unavailable clipboard write never throws or shows an error state; the button simply behaves as if nothing happened, so a denied permission can't strand the UI in a broken state.
- Button vs. hook — this component owns the full button UI (ghost styling, icon, label);
use-copy-to-clipboardowns only thecopied/errorstate machine for building a differently-shaped UI around the same copy action.