How to design copy buttons that resist rapid click spamming, swap icons without layout flashes and provide organic tactile feedback using Framer Motion and spring dynamics.
reactframer-motionuxanimationA "Copy to Clipboard" button is one of the most common micro interactions on the modern web. Every developer documentation site, code snippet wrapper and utility tool has one.
The standard layout is simple: a user clicks a button, a standard browser text write triggers and the copy icon flips to a green checkmark for a second.
But if you implement this naively, you'll hit a set of frustrating visual bugs:
- The Click Spam Flashing: If a user clicks the copy button repeatedly, the "success" timer resets and glitches. The checkmark icon flashes erratically as multiple overlapping
setTimeoutcalls fight over the state. - Layout Jumping: As the "Copy" icon (usually a double square vector) swaps out for the "Check" icon, the icon widths might differ, causing adjacent text to jitter or the button container to shift size.
- Lack of Tactile Feedback: Digital buttons should feel tactile. Without a physical press response, users can't "feel" if their click actually registered, which is why they click multiple times in the first place.
Here is a look at how to build a highly polished, rate limited CopyButton component using Framer Motion spring dynamics that solves all of these problems in under 70 lines of code.
1. Preventing Layout Jitter with PopLayout
When you swap React components inside a layout, the outgoing element is unmounted and the incoming element is mounted. For a brief split second, both elements can coexist in the DOM, pushing each other out of place.
To solve this, we wrap our active icons inside Framer Motion's
<AnimatePresence mode="popLayout">.The
popLayout mode does something magical: it takes the outgoing element completely out of the natural document flow, positioning it absolutely.This allows the incoming icon to slide into the exact same spatial coordinate without any layout shifts or container expanding.
// The incoming check icon is pushed down by the outgoing copy icon
return (
<button className="flex items-center">
{copied ? <CheckIcon /> : <CopyIcon />}
</button>
)
2. Preventing Double Trigger Glitches (Rate Limiting)
The most annoying bug in naively coded copy buttons is the "double click timer trap."
If a user clicks the button once, we set
copied to true and set a timeout for 1.5 seconds to revert it to false.
If the user clicks the button again after 1.0 second, a second async trigger fires. The state remains true, but 0.5 seconds later, the first timeout triggers, reverting the state to false prematurely, even though the second click should have kept it active!To solve this, we can design a simple click guard: if the state is already
copied, we exit early or preserve the existing state.Let's examine how the CopyButton component handles this:
/**
* A highly interactive clipboard copy button that swaps its icon with an animated checkmark upon success.
* Can be used in a controlled or uncontrolled state.
*
* @param value - The text string to copy to the clipboard.
* @param iconSize - The pixel size of the rendered icon.
* @param copied - If provided, the button acts as a controlled component reacting to this external state.
*/
export function CopyButton({
value,
iconSize = 14,
className,
onClick,
copied: controlledCopied,
...props
}: CopyButtonProps) {
const [internalCopied, setInternalCopied] = useState(false)
const isCopied = controlledCopied ?? internalCopied
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(e as any)
// 1. Guard check: If controlled state is passed, let parent handle it
if (controlledCopied !== undefined) return
try {
// 2. Perform modern Clipboard Write
await navigator.clipboard.writeText(value)
setInternalCopied(true)
// 3. Revert back to original state after 1.5 seconds
setTimeout(() => setInternalCopied(false), 1500)
} catch (err) {
console.error(err)
}
}
// ...
}
By adding the guard
if (isCopied) return, we lock the button during its active display period. Click spamming has zero effect on the timers, keeping the visual experience completely predictable.3. Creating Tactile Spring Physics
Flat, immediate state transitions make buttons feel rigid. To make our copy button feel organic, we add two layers of micro animations:
Layer A: The Click Squeeze (whileTap)
When the user presses down, the button physically squeezes in slightly (
scale: 0.94), using a high stiffness spring animation. This mimics the mechanical feedback of a physical keyboard switch:<motion.button
whileTap={{ scale: 0.94 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
onClick={handleCopy}
>
...
</motion.button>
Layer B: The Rotational Snap
As the copy icon exits, it rotates
45 degrees and fades out. Simultaneously, the success checkmark icon snaps in from a smaller scale and rotates back to 0 degrees.Because we specify an custom ease curve
[0.23, 1, 0.32, 1] (a dramatic cubic bezier), the transition feels snappy and incredibly premium.<motion.span
key={isCopied ? 'check' : 'copy'}
initial={{ scale: 0.5, opacity: 0, rotate: -45 }}
animate={{ scale: 1, opacity: 1, rotate: 0 }}
exit={{ scale: 0.5, opacity: 0, rotate: 45 }}
transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }}
>
<ActiveIcon />
</motion.span>
4. Controlled vs. Uncontrolled Flexibility
Sometimes you want the copy button to manage its own state (e.g., inside an MDX code block where the component is self contained). Other times, a parent component needs to control the state (e.g., in a complex signup flow where copying a key triggers the next step in the UI).
To handle both cases seamlessly, we support both uncontrolled internal state and controlled parent props:
const [internalCopied, setInternalCopied] = useState(false)
// If the parent passes 'copied', use it. Otherwise, fallback to local state.
const isCopied = controlledCopied ?? internalCopied
This makes our component extremely reusable across different sections of our application, allowing us to drop it into standard HTML lists or highly structured, dynamic page flows.
Polished Copy Mechanics
- Spam Proofing: Lockout window prevents overlapping animations or timer corruption.
- Layout Isolation: PopLayout absolute mechanics prevent nearby grid or flex text from shifting.
- Tactile Springiness: Organic scale physics make copying code a satisfying, responsive experience.
Summary
Premium UX is not about massive layout transitions or flashy graphics; it's about the precision of tiny interactive elements.
By taking control of layout unmounting with
<AnimatePresence mode="popLayout">, rate limiting click handlers to prevent timer racing and adding spring loaded tactile triggers, we turn a mundane copy button into a premium piece of visual feedback.