A copy button is four lines of code and three failure modes, which is a bad ratio. It is also on every developer-facing page in the world, usually next to the install command that is the first thing a visitor tries.
18 public components on 21st have "copy" in the name. The interesting part is not the animation.
The components
Copy Button by tom_ui is the blur-transition version, Copy Button - Variants by zochory crossfades between states, and Copy Button by ddoemonn animates between idle, copied and error - which is the version worth studying, because the error state is the one everyone forgets.
Copy Code Button by Le Thanh and Script Copy Button by dillionverma are the code-block variants, and useCopyToClipboard by strlrd-29 is the hook rather than the button.




The three failure modes
The clipboard API rejects. navigator.clipboard.writeText needs a secure context and, in some browsers, a user gesture it can trace. It fails on plain HTTP, inside some embedded contexts, and when a permission is denied. It returns a promise, so it must be awaited with a catch, and the catch must do something visible.
Nothing tells a screen reader. Swapping a clipboard icon for a tick communicates nothing without sight. Announce it: a live region with "Copied" is the whole fix, per the toast guide, and the button's accessible name should not change to "Copied" permanently, because then it stops describing what pressing it does.
The copied text includes the decoration. In a code block, line numbers, a leading $ prompt or diff markers must not travel with the copy. Keep the decoration in CSS or in separate elements outside the copied node, which the code block guide covers.
The details worth getting right
Reset after a moment. Two seconds back to idle. A button stuck on "Copied" is a button whose state nobody trusts on the second press.
The feedback goes on the button. Not in a toast in the corner. The reader is looking at the button they just pressed.
Copy the whole useful thing. For an install command, that is the command, not the command plus the surrounding prose. For a URL, the absolute one.
Keep the button reachable. A copy control that appears only on hover does not exist on touch and is hard to find with a keyboard. Show it always, or reveal it on focus as well as hover.
One button per copyable thing. A page with six code blocks and one copy button at the top is a page where five copies are wrong.




Where else to look
The honest list, because the answer is not always us:
| Source | Best for | Trade-off |
|---|---|---|
navigator.clipboard | The API itself, in four lines | Needs a secure context and a real catch |
| A hook | Reusing the state machine across many buttons | Still your visual layer |
| shadcn/ui | Button and Tooltip underneath | No copy behaviour |
| 21st | Animated variants and the code-block versions | Quality varies by author, so preview before you take it |
Taking one
Every component page has a live preview and the code. Installing goes through the shadcn CLI against our registry:
That key comes from your 21st account, and installs require a membership. Set API_KEY_21ST once in your shell and the command works for anything in the catalogue.
Frequently asked
- Why does copy to clipboard fail sometimes?
- navigator.clipboard.writeText needs a secure context and, in some browsers, a traceable user gesture. It fails on plain HTTP, inside some embedded contexts and when permission is denied. It returns a promise, so it needs a catch, and the catch needs a visible error state.
- How do you make a copy button accessible?
- Announce the result in a live region rather than only swapping an icon, since an icon change communicates nothing without sight. Keep the button's accessible name describing the action rather than permanently changing it to Copied, and reset the visual state after about two seconds.
- Where should the copied feedback appear?
- On the button, because that is where the reader is looking. A toast in the opposite corner for an action taken at the cursor is a longer path to the same information.
- What should a copy button in a code block copy?
- The code and nothing else. Line numbers, prompt characters and diff markers must live in CSS or outside the copied element, or they travel with the paste. Give each copyable block its own button.