Build a Copy to Clipboard Button
Learn how to use the async Clipboard API, manage feedback states, and handle permission errors gracefully.
Table of Contents
Problem Statement
Build a copy button that:
- Copies text to the clipboard on click
- Shows visual feedback: idle → copied → idle
- Handles errors (permission denied, insecure context)
- Resets back to idle after 2 seconds
- Works independently per instance (multiple buttons on page)
Why this question?
It's a small, focused problem that tests async/await, state transitions, and cleanup (clearing timeouts). Interviewers use it to see if you handle edge cases like errors and stale timers.
The Clipboard API
The modern Clipboard API is promise-based and requires a secure context (HTTPS or localhost):
// Write text
await navigator.clipboard.writeText("hello");
// Read text (requires permission)
const text = await navigator.clipboard.readText();
Fallback
If you need to support older browsers, fall back to document.execCommand("copy") with a hidden textarea — but in an interview, the async API is the expected answer.
Feedback State Machine
The button has three states: idle, copied, and error. A timeout resets it:
idle ──click──→ copied ──2s──→ idle
└──error──→ error ──2s──→ idle
type Status = "idle" | "copied" | "error";
const [status, setStatus] = useState<Status>("idle");
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setStatus("copied");
} catch {
setStatus("error");
}
setTimeout(() => setStatus("idle"), 2000);
};
Cleanup
If the component unmounts before the timeout fires, you get a "set state on unmounted component" warning. Store the timeout ID in a ref and clear it on unmount.
Accessibility
- Use aria-label to describe the action (e.g. "Copy install command")
- Announce feedback with aria-live="polite" or role="status"
- Don't rely on color alone — change the text/icon too
- Button should remain focusable during all states
Interview Follow-up Questions
Q:How do you prevent stale timeouts if the user clicks multiple times quickly?
A: Store the timeout ID in a ref. Before setting a new timeout, clear the previous one with clearTimeout(ref.current). This ensures only the latest click's timer is active.
Q:What happens if the Clipboard API isn't available?
A: navigator.clipboard is undefined in insecure contexts (plain HTTP). Check if it exists before calling writeText, and show an error state or fall back to document.execCommand('copy') with a temporary textarea.
Q:How would you make this into a reusable hook?
A: Extract into useCopyToClipboard(text) that returns { status, copy }. The hook owns the state and timeout cleanup via useEffect return. The component just renders based on status and calls copy on click.
Ready to build it yourself?
The snippets and button shell are ready. Implement the CopyButton — add state, call the Clipboard API, and show feedback.