Copy Code Button
Terminal-style snippet with an animated copy confirmation.
$ npm i motion lucide-react$ npx create-next-app@latest my-appcomponents/ui/copy-code-button.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { Check, Copy } from "lucide-react";
import { useState } from "react";
export function CopyCodeButton({ text, label = "Copy" }: { text: string; label?: string }) {
const [copied, setCopied] = useState(false);
async function copy() {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch {
/* Clipboard blocked (e.g. insecure context) — leave state unchanged. */
}
}
return (
<button onClick={copy} aria-label={copied ? "Copied" : `${label} to clipboard`} className="relative inline-flex h-8 items-center gap-1.5 overflow-hidden rounded-lg border border-zinc-700 bg-zinc-800 px-2.5 text-xs font-medium text-zinc-200 transition-colors hover:bg-zinc-700">
<AnimatePresence mode="wait" initial={false}>
<motion.span key={copied ? "y" : "n"} initial={{ y: 12, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -12, opacity: 0 }} transition={{ duration: 0.15 }} className="flex items-center gap-1.5">
{copied ? <Check className="size-3.5 text-emerald-400" /> : <Copy className="size-3.5" />}
{copied ? "Copied!" : label}
</motion.span>
</AnimatePresence>
</button>
);
}
export function CodeSnippet({ code }: { code: string }) {
return (
<div className="flex items-center justify-between gap-4 rounded-xl bg-zinc-900 py-2 pl-4 pr-2 font-mono text-sm text-zinc-100">
<code className="truncate">
<span className="select-none text-zinc-500">$ </span>
{code}
</code>
<CopyCodeButton text={code} />
</div>
);
}
export default function Demo() {
return (
<div className="w-full max-w-md space-y-3">
<CodeSnippet code="npm i motion lucide-react" />
<CodeSnippet code="npx create-next-app@latest my-app" />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/copy-code-button.tsxcomponents/ui/copy-code-button.tsx"use client"; import { AnimatePresence, motion } from "motion/react"; import { Check, Copy } from "lucide-react"; import { useState } from "react"; export function CopyCodeButton({ text, label = "Copy" }: { text: string; label?: string }) { const [copied, setCopied] = useState(false); async function copy() { try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1600); } catch { /* Clipboard blocked (e.g. insecure context) — leave state unchanged. */ } } return ( <button onClick={copy} aria-label={copied ? "Copied" : `${label} to clipboard`} className="relative inline-flex h-8 items-center gap-1.5 overflow-hidden rounded-lg border border-zinc-700 bg-zinc-800 px-2.5 text-xs font-medium text-zinc-200 transition-colors hover:bg-zinc-700"> <AnimatePresence mode="wait" initial={false}> <motion.span key={copied ? "y" : "n"} initial={{ y: 12, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -12, opacity: 0 }} transition={{ duration: 0.15 }} className="flex items-center gap-1.5"> {copied ? <Check className="size-3.5 text-emerald-400" /> : <Copy className="size-3.5" />} {copied ? "Copied!" : label} </motion.span> </AnimatePresence> </button> ); } export function CodeSnippet({ code }: { code: string }) { return ( <div className="flex items-center justify-between gap-4 rounded-xl bg-zinc-900 py-2 pl-4 pr-2 font-mono text-sm text-zinc-100"> <code className="truncate"> <span className="select-none text-zinc-500">$ </span> {code} </code> <CopyCodeButton text={code} /> </div> ); } export default function Demo() { return ( <div className="w-full max-w-md space-y-3"> <CodeSnippet code="npm i motion lucide-react" /> <CodeSnippet code="npx create-next-app@latest my-app" /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.